Repository: scalableminds/webknossos
Pull request #8602: Don't label outside of layer's bbox
Author: login: philippotto
Created: 2025-05-06T14:14:56Z | Merged: 2025-05-19T12:59:30Z

This PR takes care that buckets that are only partially contained by the layer's bbox can only be mutated within the area that is included in the bbox.
Additionally, it refactors the api `labelVoxels` function to not use deprecated code anymore.

### URL of deployed dev instance (used for testing):
- https://___.webknossos.xyz

### Steps to test:
- edit an dataset and make it's bounding box a bit smaller on all edges (only by a few pixels so that we can test partial buckets)
- open an annotation for that dataset and try to label outside of the bbox
- the brush should not mutate any voxels outside of the bbox
- test the api
  - use the api in the console to mutate data (e.g., with `webknossos.DEV.api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);`)
- repeat both test scenarios for an annotation with restricted mags (e.g., no mag 1)

### Issues:
- fixes #6303 

------
(Please delete unneeded items, merge only when none are left open)
- [x] Updated [changelog](../blob/master/CHANGELOG.unreleased.md#unreleased)
- [ ] Updated [migration guide](../blob/master/MIGRATIONS.unreleased.md#unreleased) if applicable
- [ ] 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


Linked issue #6303: Don't label data outside of dataset bbox
Labeling volume data outside of the dataset's bbox does not produce visible labels in the viewports. However, ad-hoc meshing the segment shows that data is stored outside of the dataset's bbox. Ideally, this data should not be produced so that other code (such as meshing or download) does not handle this data which the user has never seen before.
Linked issue comments: user: login: philippotto
created_at: 2022-06-23T14:24:52Z
body: I think, buckets which are completely outside of the DS bbox are not created, anyway. However, buckets which overlap are probably created and bbox checks would need to be introduce dwhen labeling these.
author_association: MEMBER

Diff:
diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 3143b194c12..5a82f4a9227 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -33,6 +33,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
 - Fixed that the minimum size of bounding boxes for AI neuron and mitochondria inferral was not checked before starting the job. [#8561](https://github.com/scalableminds/webknossos/pull/8561)
 - Fixed that layer bounding boxes were sometimes colored green even though this should only happen for tasks. [#8535](https://github.com/scalableminds/webknossos/pull/8535)
 - Fixed that annotations could not be opened anymore (caused by #8535). [#8599](https://github.com/scalableminds/webknossos/pull/8599)
+- Voxels outside of the layer bounding box cannot be brushed, anymore. [#8602](https://github.com/scalableminds/webknossos/pull/8602)
 - The guest tag is now also shown for guest admin users. [#8612](https://github.com/scalableminds/webknossos/pull/8612)
 - Fixed a rare bug where segment bounding box would not be displayed correctly, with the request potentially even crashing the server. [#8590](https://github.com/scalableminds/webknossos/pull/8590)
 - Fixed a rare bug where download requests would terminate without sending the whole annotation. [#8624](https://github.com/scalableminds/webknossos/pull/8624)
diff --git a/docs/volume_annotation/segments_list.md b/docs/volume_annotation/segments_list.md
index 85d1e7f10a7..83780eda102 100644
--- a/docs/volume_annotation/segments_list.md
+++ b/docs/volume_annotation/segments_list.md
@@ -6,6 +6,7 @@ The following functionality is available for each segment:
 
 - jumping to the segment (via left-click; this uses the position at which the segment was initially registered)
 - naming the segment
+- toggling the visibility of the segment (the visibility of unlisted segments is controlled with the `Hide unlisted segments` setting in the left sidebar in the `Layers` tab)
 - [loading 3D meshes](../meshes/loading_meshes.md) for the segments (ad-hoc and precomputed if available)
 - download of 3D meshes
 - changing the color of the segment, and if its mesh is visible, changing the mesh's opacity using the opacity slider of the segment color picker
@@ -15,4 +16,4 @@ The following functionality is available for each segment:
 ![youtube-video](https://www.youtube.com/embed/BJ7lblTSVKY)
 
 Working with groups allows you to perform batch actions of a whole group of segment (e.g. changing the color, loading meshes, ...)
-![youtube-video](https://www.youtube.com/embed/lz-3kFWQ2H8)
\ No newline at end of file
+![youtube-video](https://www.youtube.com/embed/lz-3kFWQ2H8)
diff --git a/frontend/javascripts/libs/utils.ts b/frontend/javascripts/libs/utils.ts
index 05bffa12a82..8de352dfd43 100644
--- a/frontend/javascripts/libs/utils.ts
+++ b/frontend/javascripts/libs/utils.ts
@@ -16,6 +16,7 @@ import type {
 import type { BoundingBoxObject, NumberLike } from "viewer/store";
 
 type UrlParams = Record<string, string>;
+
 // Fix JS modulo bug
 // http://javascript.about.com/od/problemsolving/a/modulobug.htm
 export function mod(x: number, n: number) {
diff --git a/frontend/javascripts/test/helpers/apiHelpers.ts b/frontend/javascripts/test/helpers/apiHelpers.ts
index 570d0d76a09..1cba1ded9c0 100644
--- a/frontend/javascripts/test/helpers/apiHelpers.ts
+++ b/frontend/javascripts/test/helpers/apiHelpers.ts
@@ -190,8 +190,7 @@ export async function setupWebknossosForTesting(
   testContext.setSlowCompression = setSlowCompression;
   testContext.tearDownPullQueues = () =>
     Model.getAllLayers().map((layer) => {
-      layer.pullQueue.clear();
-      layer.pullQueue.abortRequests();
+      layer.pullQueue.destroy();
     });
 
   const webknossos = new WebknossosApi(Model);
diff --git a/frontend/javascripts/test/model/binary/cube.spec.ts b/frontend/javascripts/test/model/binary/cube.spec.ts
index 04bb7a76740..1186f9c1035 100644
--- a/frontend/javascripts/test/model/binary/cube.spec.ts
+++ b/frontend/javascripts/test/model/binary/cube.spec.ts
@@ -86,7 +86,7 @@ describe("DataCube", () => {
         await sleep(10);
 
         for (const item of this.queue) {
-          const bucket = cube.getBucket(item.bucket, true);
+          const bucket = cube.getBucket(item.bucket);
 
           if (bucket.type === "data") {
             bucket.markAsRequested();
@@ -269,3 +269,46 @@ describe("DataCube", () => {
     expect(index).toBe(10570);
   });
 });
+
+// This is not executed in the tests, but can be activated when needed
+// to make performance measurements for getOrCreateBucket
+describe.skip("DataCube Benchmark", () => {
+  it("Benchmark", () => {
+    const mockedLayer = {
+      resolutions: [[1, 1, 1]] as Vector3[],
+    };
+    const magInfo = new MagInfo(mockedLayer.resolutions);
+    const cube = new DataCube(
+      new BoundingBox({ min: [1024, 1024, 1024], max: [2048, 2048, 2048] }),
+      [],
+      magInfo,
+      "uint32",
+      false,
+      "layerName",
+    );
+
+    console.time("outside");
+    for (let i = 0; i < 15; i++) {
+      for (let x = 0; x < 32; x++) {
+        for (let y = 0; y < 32; y++) {
+          for (let z = 0; z < 32; z++) {
+            cube.getOrCreateBucket([x, y, z, 0]);
+          }
+        }
+      }
+    }
+    console.timeEnd("outside");
+
+    console.time("inside");
+    for (let i = 0; i < 15; i++) {
+      for (let x = 32; x < 64; x++) {
+        for (let y = 32; y < 64; y++) {
+          for (let z = 32; z < 64; z++) {
+            cube.getOrCreateBucket([x, y, z, 0]);
+          }
+        }
+      }
+    }
+    console.timeEnd("inside");
+  });
+});
diff --git a/frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts b/frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts
index cda7139af77..f2f84597055 100644
--- a/frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts
+++ b/frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts
@@ -250,12 +250,24 @@ describe("wkstore_adapter", () => {
 
   it<TestContext>("sendToStore: Request Handling should send the correct request parameters", () => {
     const data = new Uint8Array(2);
-    const bucket1 = new DataBucket("uint8", [0, 0, 0, 0], null as any, mockedCube);
+    const bucket1 = new DataBucket(
+      "uint8",
+      [0, 0, 0, 0],
+      null as any,
+      { type: "full" },
+      mockedCube,
+    );
 
     bucket1.markAsRequested();
     bucket1.receiveData(data);
 
-    const bucket2 = new DataBucket("uint8", [1, 1, 1, 1], null as any, mockedCube);
+    const bucket2 = new DataBucket(
+      "uint8",
+      [1, 1, 1, 1],
+      null as any,
+      { type: "full" },
+      mockedCube,
+    );
     bucket2.markAsRequested();
     bucket2.receiveData(data);
 
diff --git a/frontend/javascripts/test/model/binary/pullqueue.spec.ts b/frontend/javascripts/test/model/binary/pullqueue.spec.ts
index 17e9937a2f8..bd154d4cf6f 100644
--- a/frontend/javascripts/test/model/binary/pullqueue.spec.ts
+++ b/frontend/javascripts/test/model/binary/pullqueue.spec.ts
@@ -83,8 +83,8 @@ describe("PullQueue", () => {
     const pullQueue = new PullQueue(mockedCube as any, "layername", datastoreInfo as any);
     mockedCube.pullQueue = pullQueue;
     const buckets = [
-      new DataBucket("uint8", [0, 0, 0, 0], null as any, mockedCube as any),
-      new DataBucket("uint8", [1, 1, 1, 1], null as any, mockedCube as any),
+      new DataBucket("uint8", [0, 0, 0, 0], null as any, { type: "full" }, mockedCube as any),
+      new DataBucket("uint8", [1, 1, 1, 1], null as any, { type: "full" }, mockedCube as any),
     ];
 
     mockedCube.getBucket.mockImplementation((address: BucketAddress) => {
diff --git a/frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts b/frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts
index 690a1e45d02..56b59c8a719 100644
--- a/frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts
+++ b/frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts
@@ -53,14 +53,14 @@ describe("TemporalBucketManager", () => {
   }
 
   it<TestContext>("should be added when bucket has not been requested", ({ manager, cube }) => {
-    const bucket = new DataBucket("uint8", [0, 0, 0, 0], manager, cube);
+    const bucket = new DataBucket("uint8", [0, 0, 0, 0], manager, { type: "full" }, cube);
 
     fakeLabel(bucket);
     expect(manager.getCount()).toBe(1);
   });
 
   it<TestContext>("should be added when bucket has not been received", ({ manager, cube }) => {
-    const bucket = new DataBucket("uint8", [0, 0, 0, 0], manager, cube);
+    const bucket = new DataBucket("uint8", [0, 0, 0, 0], manager, { type: "full" }, cube);
 
     bucket.markAsRequested();
     expect(bucket.needsRequest()).toBe(false);
@@ -70,7 +70,7 @@ describe("TemporalBucketManager", () => {
   });
 
   it<TestContext>("should not be added when bucket has been received", ({ manager, cube }) => {
-    const bucket = new DataBucket("uint8", [0, 0, 0, 0], manager, cube);
+    const bucket = new DataBucket("uint8", [0, 0, 0, 0], manager, { type: "full" }, cube);
     bucket.markAsRequested();
     bucket.receiveData(new Uint8Array(1 << 15));
 
@@ -81,7 +81,7 @@ describe("TemporalBucketManager", () => {
   });
 
   it<TestContext>("should be removed once it is loaded", ({ manager, cube }) => {
-    const bucket = new DataBucket("uint8", [0, 0, 0, 0], manager, cube);
+    const bucket = new DataBucket("uint8", [0, 0, 0, 0], manager, { type: "full" }, cube);
     fakeLabel(bucket);
 
     bucket.markAsRequested();
@@ -93,8 +93,8 @@ describe("TemporalBucketManager", () => {
   // Helper function to prepare buckets
   function prepareBuckets(manager: TemporalBucketManager, cube: any) {
     // Insert two buckets into manager
-    const bucket1 = new DataBucket("uint8", [0, 0, 0, 0], manager, cube);
-    const bucket2 = new DataBucket("uint8", [1, 0, 0, 0], manager, cube);
+    const bucket1 = new DataBucket("uint8", [0, 0, 0, 0], manager, { type: "full" }, cube);
+    const bucket2 = new DataBucket("uint8", [1, 0, 0, 0], manager, { type: "full" }, cube);
 
     for (const bucket of [bucket1, bucket2]) {
       bucket.startDataMutation();
diff --git a/frontend/javascripts/test/model/texture_bucket_manager.spec.ts b/frontend/javascripts/test/model/texture_bucket_manager.spec.ts
index 65c9b622b9e..4a91f49c0d5 100644
--- a/frontend/javascripts/test/model/texture_bucket_manager.spec.ts
+++ b/frontend/javascripts/test/model/texture_bucket_manager.spec.ts
@@ -35,6 +35,7 @@ const buildBucket = (zoomedAddress: Vector4, firstByte: number) => {
     "uint8",
     zoomedAddress,
     temporalBucketManagerMock as any,
+    { type: "full" },
     mockedCube as any,
   );
   bucket._fallbackBucket = NULL_BUCKET;
diff --git a/frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts b/frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts
index 2fba8861f69..10454a962cc 100644
--- a/frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts
+++ b/frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts
@@ -1,6 +1,6 @@
 import { tracing as skeletontracingServerObject } from "test/fixtures/skeletontracing_server_objects";
 import { tracing as volumetracingServerObject } from "test/fixtures/volumetracing_server_objects";
-import type { Vector3, Vector4 } from "viewer/constants";
+import type { LabeledVoxelsMap, Vector3, Vector4 } from "viewer/constants";
 import Constants from "viewer/constants";
 import { describe, it, beforeEach, vi, expect } from "vitest";
 import datasetServerObject from "test/fixtures/dataset_server_object";
@@ -8,6 +8,10 @@ import { MagInfo } from "viewer/model/helpers/mag_info";
 import BoundingBox from "viewer/model/bucket_data_handling/bounding_box";
 import type DataCubeType from "viewer/model/bucket_data_handling/data_cube";
 import { assertNonNullBucket } from "viewer/model/bucket_data_handling/bucket";
+import DataCube from "viewer/model/bucket_data_handling/data_cube";
+import sampleVoxelMapToMag, {
+  applyVoxelMap,
+} from "viewer/model/volumetracing/volume_annotation_sampling";
 
 // Mock modules
 vi.mock("viewer/store", () => {
@@ -39,12 +43,6 @@ vi.mock("viewer/model/sagas/root_saga", () => {
 
 type LabeledVoxelsMapAsArray = Array<[Vector4, Uint8Array]>;
 
-// Import the modules after mocking
-import DataCube from "viewer/model/bucket_data_handling/data_cube";
-import sampleVoxelMapToMag, {
-  applyVoxelMap,
-} from "viewer/model/volumetracing/volume_annotation_sampling";
-
 // Test context type
 type TestContext = {
   cube: DataCubeType;
@@ -66,6 +64,8 @@ function getVoxelMapEntry(firstDim: number, secondDim: number, voxelMap: Uint8Ar
 describe("Volume Annotation Sampling", () => {
   let context: TestContext;
 
+  const cubeBoundingBox = new BoundingBox({ min: [1, 2, 3], max: [1023, 1024, 1025] });
+
   beforeEach(() => {
     const mockedLayer = {
       resolutions: [
@@ -78,14 +78,7 @@ describe("Volume Annotation Sampling", () => {
       ] as Vector3[],
     };
     const magInfo = new MagInfo(mockedLayer.resolutions);
-    const cube = new DataCube(
-      new BoundingBox({ min: [0, 0, 0], max: [1024, 1024, 1024] }),
-      [],
-      magInfo,
-      "uint32",
-      false,
-      "layerName",
-    );
+    const cube = new DataCube(cubeBoundingBox, [], magInfo, "uint32", false, "layerName");
     const pullQueue = {
       add: vi.fn(),
       pull: vi.fn(),
@@ -94,7 +87,7 @@ describe("Volume Annotation Sampling", () => {
       insert: vi.fn(),
       push: vi.fn(),
     };
-    // @ts-expect-error
+    // @ts-ignore
     cube.initializeWithQueues(pullQueue, pushQueue);
     context = {
       cube,
@@ -603,7 +596,7 @@ describe("Volume Annotation Sampling", () => {
     const { cube } = context;
     const bucket = cube.getOrCreateBucket([0, 0, 0, 0]);
     assertNonNullBucket(bucket);
-    const labeledVoxelsMap = new Map();
+    const labeledVoxelsMap: LabeledVoxelsMap = new Map();
     const voxelMap = getEmptyVoxelMap();
     const voxelsToLabel = [
       [10, 10],
@@ -615,6 +608,51 @@ describe("Volume Annotation Sampling", () => {
       [11, 12],
       [11, 13],
     ];
+    const Z = 5;
+    voxelsToLabel.forEach(([firstDim, secondDim]) =>
+      labelVoxelInVoxelMap(firstDim, secondDim, voxelMap),
+    );
+    labeledVoxelsMap.set(bucket.zoomedAddress, voxelMap);
+
+    const get3DAddress = (x: number, y: number, out: Vector3 | Float32Array) => {
+      out[0] = x;
+      out[1] = y;
+      out[2] = Z;
+    };
+
+    const expectedBucketData = new Uint32Array(Constants.BUCKET_SIZE).fill(0);
+    voxelsToLabel.forEach(([firstDim, secondDim]) => {
+      const addr = cube.getVoxelIndex([firstDim, secondDim, Z], 0);
+      expectedBucketData[addr] = 1;
+    });
+    applyVoxelMap(labeledVoxelsMap, cube, 1, get3DAddress, 1, 2, true);
+    const labeledBucketData = bucket.getOrCreateData();
+
+    for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
+      for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
+        const addr = cube.getVoxelIndex([firstDim, secondDim, Z], 0);
+        expect(labeledBucketData[addr]).toBe(expectedBucketData[addr]);
+      }
+    }
+  });
+
+  it("A labeledVoxelMap should be applied correctly (ignore values outside of bbox)", () => {
+    const { cube } = context;
+    const bucket = cube.getOrCreateBucket([0, 0, 0, 0]);
+    assertNonNullBucket(bucket);
+    const labeledVoxelsMap: LabeledVoxelsMap = new Map();
+    const voxelMap = getEmptyVoxelMap();
+    const voxelsToLabel = [
+      [0, 0],
+      [0, 1],
+      [1, 0],
+      [1, 1],
+      [2, 0],
+      [2, 1],
+      [2, 2],
+      [3, 0],
+    ];
+    const Z = 5;
     voxelsToLabel.forEach(([firstDim, secondDim]) =>
       labelVoxelInVoxelMap(firstDim, secondDim, voxelMap),
     );
@@ -623,12 +661,20 @@ describe("Volume Annotation Sampling", () => {
     const get3DAddress = (x: number, y: number, out: Vector3 | Float32Array) => {
       out[0] = x;
       out[1] = y;
-      out[2] = 5;
+      out[2] = Z;
     };
 
     const expectedBucketData = new Uint32Array(Constants.BUCKET_SIZE).fill(0);
     voxelsToLabel.forEach(([firstDim, secondDim]) => {
-      const addr = cube.getVoxelIndex([firstDim, secondDim, 5], 0);
+      if (
+        firstDim < cubeBoundingBox.min[0] ||
+        secondDim < cubeBoundingBox.min[1] ||
+        firstDim >= cubeBoundingBox.max[0] ||
+        secondDim >= cubeBoundingBox.max[1]
+      ) {
+        return;
+      }
+      const addr = cube.getVoxelIndex([firstDim, secondDim, Z], 0);
       expectedBucketData[addr] = 1;
     });
     applyVoxelMap(labeledVoxelsMap, cube, 1, get3DAddress, 1, 2, true);
@@ -636,7 +682,47 @@ describe("Volume Annotation Sampling", () => {
 
     for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
       for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
-        const addr = cube.getVoxelIndex([firstDim, secondDim, 5], 0);
+        const addr = cube.getVoxelIndex([firstDim, secondDim, Z], 0);
+        expect(labeledBucketData[addr]).toBe(expectedBucketData[addr]);
+      }
+    }
+  });
+
+  it("A labeledVoxelMap should be applied correctly (ignore all values outside of z)", () => {
+    const { cube } = context;
+    const bucket = cube.getOrCreateBucket([0, 0, 0, 0]);
+    assertNonNullBucket(bucket);
+    const labeledVoxelsMap: LabeledVoxelsMap = new Map();
+    const voxelMap = getEmptyVoxelMap();
+    const voxelsToLabel = [
+      [0, 0],
+      [0, 1],
+      [1, 0],
+      [1, 1],
+      [2, 0],
+      [2, 1],
+      [2, 2],
+      [3, 0],
+    ];
+    const Z = 0;
+    voxelsToLabel.forEach(([firstDim, secondDim]) =>
+      labelVoxelInVoxelMap(firstDim, secondDim, voxelMap),
+    );
+    labeledVoxelsMap.set(bucket.zoomedAddress, voxelMap);
+
+    const get3DAddress = (x: number, y: number, out: Vector3 | Float32Array) => {
+      out[0] = x;
+      out[1] = y;
+      out[2] = Z;
+    };
+
+    const expectedBucketData = new Uint32Array(Constants.BUCKET_SIZE).fill(0);
+    applyVoxelMap(labeledVoxelsMap, cube, 1, get3DAddress, 1, 2, true);
+    const labeledBucketData = bucket.getOrCreateData();
+
+    for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
+      for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
+        const addr = cube.getVoxelIndex([firstDim, secondDim, Z], 0);
         expect(labeledBucketData[addr]).toBe(expectedBucketData[addr]);
       }
     }
diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts
index f5649b3d371..95db39644d8 100644
--- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts
+++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts
@@ -449,7 +449,7 @@ describe("Volume Tracing", () => {
     Group 2
       Segment 3
       Segment 4
-  */
+    */
     Store.dispatch(
       setSegmentGroupsAction(
         [
diff --git a/frontend/javascripts/types/globals.d.ts b/frontend/javascripts/types/globals.d.ts
index 7ff3de1d0c6..20408fb59fd 100644
--- a/frontend/javascripts/types/globals.d.ts
+++ b/frontend/javascripts/types/globals.d.ts
@@ -11,6 +11,7 @@ declare global {
   }
 }
 
+// Typescript utility types:
 // https://stackoverflow.com/questions/49285864/is-there-a-valueof-similar-to-keyof-in-typescript
 export type ValueOf<T> = T[keyof T];
 export type EmptyObject = Record<string, never>;
@@ -21,3 +22,4 @@ export type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
 export type Mutable<T> = {
   -readonly [K in keyof T]: T[K];
 };
+export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts
index 2f26d77ab49..6a265ee2da1 100644
--- a/frontend/javascripts/viewer/api/api_latest.ts
+++ b/frontend/javascripts/viewer/api/api_latest.ts
@@ -13,17 +13,19 @@ import type { ToastStyle } from "libs/toast";
 import Toast from "libs/toast";
 import UserLocalStorage from "libs/user_local_storage";
 import * as Utils from "libs/utils";
-import { coalesce } from "libs/utils";
+import { coalesce, mod } from "libs/utils";
 import window, { location } from "libs/window";
 import _ from "lodash";
 import messages from "messages";
 import TWEEN from "tween.js";
 import { type APICompoundType, APICompoundTypeEnum, type ElementClass } from "types/api_types";
 import type { AdditionalCoordinate } from "types/api_types";
+import type { Writeable } from "types/globals";
 import type {
   BoundingBoxType,
   BucketAddress,
   ControlMode,
+  LabeledVoxelsMap,
   OrthoView,
   TypedArray,
   Vector3,
@@ -72,6 +74,7 @@ import {
 } from "viewer/model/accessors/skeletontracing_accessor";
 import { AnnotationTool, type AnnotationToolId } from "viewer/model/accessors/tool_accessor";
 import {
+  enforceActiveVolumeTracing,
   getActiveCellId,
   getActiveSegmentationTracing,
   getNameOfRequestedOrVisibleSegmentationLayer,
@@ -134,6 +137,7 @@ import {
   type BatchableUpdateSegmentAction,
   batchUpdateGroupsAndSegmentsAction,
   clickSegmentAction,
+  finishAnnotationStrokeAction,
   removeSegmentAction,
   setActiveCellAction,
   setSegmentGroupsAction,
@@ -142,6 +146,7 @@ import {
 import BoundingBox from "viewer/model/bucket_data_handling/bounding_box";
 import type { Bucket, DataBucket } from "viewer/model/bucket_data_handling/bucket";
 import type DataLayer from "viewer/model/data_layer";
+import Dimensions from "viewer/model/dimensions";
 import dimensions from "viewer/model/dimensions";
 import { MagInfo } from "viewer/model/helpers/mag_info";
 import { parseNml } from "viewer/model/helpers/nml_helpers";
@@ -151,10 +156,13 @@ import {
   globalPositionToBucketPosition,
   scaleGlobalPositionWithMagnification,
   zoomedAddressToZoomedPosition,
+  zoomedPositionToZoomedAddress,
 } from "viewer/model/helpers/position_converter";
 import { getConstructorForElementClass } from "viewer/model/helpers/typed_buffer";
 import { getMaximumGroupId } from "viewer/model/reducers/skeletontracing_reducer_helpers";
 import { getHalfViewportExtentsInUnitFromState } from "viewer/model/sagas/saga_selectors";
+import { applyLabeledVoxelMapToAllMissingMags } from "viewer/model/sagas/volume/helpers";
+import { applyVoxelMap } from "viewer/model/volumetracing/volume_annotation_sampling";
 import { Model, api } from "viewer/singletons";
 import type {
   DatasetConfiguration,
@@ -412,7 +420,8 @@ class TracingApi {
    * @example
    * const comment = api.tracing.getCommentForNode(23);
    *
-   * @example // Provide a tree for lookup speed boost
+   * @example
+   * // Provide a tree for lookup speed boost
    * const comment = api.tracing.getCommentForNode(23, api.getActiveTreeid());
    */
   getCommentForNode(nodeId: number, treeId?: number): string | null | undefined {
@@ -1814,14 +1823,17 @@ class DataApi {
    * If the zoom level is provided and points to a not existent mag,
    * 0 will be returned.
    *
-   * @example // Return the greyscale value for a bucket
+   * @example
+   * // Return the greyscale value for a bucket
    * const position = [123, 123, 123];
    * api.data.getDataValue("binary", position).then((greyscaleColor) => ...);
    *
-   * @example // Using the await keyword instead of the promise syntax
+   * @example
+   * // Using the await keyword instead of the promise syntax
    * const greyscaleColor = await api.data.getDataValue("binary", position);
    *
-   * @example // Get the segmentation id for the first volume tracing layer
+   * @example
+   * // Get the segmentation id for the first volume tracing layer
    * const segmentId = await api.data.getDataValue(api.data.getVolumeTracingLayerIds()[0], position);
    */
   async getDataValue(
@@ -1995,7 +2007,7 @@ class DataApi {
       bucketPositionToGlobalAddress(bucketAddress, new MagInfo(magnifications));
 
     const nextBucketInDim = (bucket: BucketAddress, dim: 0 | 1 | 2) => {
-      const copy = bucket.slice() as BucketAddress;
+      const copy = bucket.slice() as Writeable<BucketAddress>;
       copy[dim]++;
       return copy;
     };
@@ -2120,7 +2132,8 @@ class DataApi {
    * Downloads a cuboid of raw data from a dataset (not tracing) layer. A new window is opened for the download -
    * if that is not the case, please check your pop-up blocker.
    *
-   * @example // Download a cuboid (from (0, 0, 0) to (100, 200, 100)) of raw data from the "segmentation" layer.
+   * @example
+   * // Download a cuboid (from (0, 0, 0) to (100, 200, 100)) of raw data from the "segmentation" layer.
    * api.data.downloadRawDataCuboid("segmentation", [0,0,0], [100,200,100]);
    */
   downloadRawDataCuboid(layerName: string, topLeft: Vector3, bottomRight: Vector3): Promise<void> {
@@ -2156,32 +2169,115 @@ class DataApi {
   }
 
   /**
-   * Label voxels with the supplied value. Note that this method does not mutate
-   * the data immediately, but instead returns a promise (since the data might
-   * have to be downloaded first).
+   * Label voxels with the supplied value. This method behaves as if the user
+   * had brushed the provided voxels all at once. If the volume data wasn't
+   * downloaded completely yet, WEBKNOSSOS will merge the data as soon as it
+   * was downloaded.
    *
    * _Volume tracing only!_
    *
-   * @example // Set the segmentation id for some voxels to 1337
-   * await api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
+   * @example
+   * // Set the segmentation id for some voxels to 1337
+   * api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
    */
-  async labelVoxels(
-    voxels: Array<Vector3>,
-    label: number,
-    additionalCoordinates: AdditionalCoordinate[] | null = null,
-  ): Promise<void> {
-    assertVolume(Store.getState());
-    const segmentationLayer = this.model.getEnforcedSegmentationTracingLayer();
-    await Promise.all(
-      voxels.map((voxel) =>
-        segmentationLayer.cube._labelVoxelInAllResolutions_DEPRECATED(
-          voxel,
+  labelVoxels(
+    globalPositionsMag1: Vector3[],
+    segmentId: number,
+    optAdditionalCoordinates?: AdditionalCoordinate[] | null,
+  ) {
+    const state = Store.getState();
+    const allowUpdate = state.annotation.restrictions.allowUpdate;
+    const additionalCoordinates =
+      optAdditionalCoordinates === undefined
+        ? state.flycam.additionalCoordinates
+        : optAdditionalCoordinates;
+    if (!allowUpdate || globalPositionsMag1.length === 0) return;
+
+    const volumeTracing = enforceActiveVolumeTracing(state);
+    const segmentationLayer = Model.getSegmentationTracingLayer(volumeTracing.tracingId);
+    const { cube } = segmentationLayer;
+    const magInfo = getMagInfo(segmentationLayer.mags);
+    const labeledZoomStep = magInfo.getClosestExistingIndex(0);
+    const labeledMag = magInfo.getMagByIndexOrThrow(labeledZoomStep);
+    const dimensionIndices = Dimensions.getIndices(OrthoViews.PLANE_XY);
+    const thirdDim = dimensionIndices[2];
+
+    const globalPositions = globalPositionsMag1.map(
+      (pos): Vector3 => [
+        Math.floor(pos[0] / labeledMag[0]),
+        Math.floor(pos[1] / labeledMag[1]),
+        Math.floor(pos[2] / labeledMag[2]),
+      ],
+    );
+    const groupedByW = _.groupBy(globalPositions, (pos) => pos[thirdDim]);
+
+    for (const group of Object.values(groupedByW)) {
+      const w = group[0][thirdDim];
+      const currentLabeledVoxelMap: LabeledVoxelsMap = new Map();
+
+      for (const pos of group) {
+        const bucketZoomedAddress = zoomedPositionToZoomedAddress(
+          pos,
+          labeledZoomStep,
           additionalCoordinates,
-          label,
-        ),
+        );
+
+        let labelMap = currentLabeledVoxelMap.get(bucketZoomedAddress);
+        if (!labelMap) {
+          labelMap = new Uint8Array(Constants.BUCKET_WIDTH ** 2);
+          currentLabeledVoxelMap.set(bucketZoomedAddress, labelMap);
+        }
+
+        const a = pos[dimensionIndices[0]];
+        const b = pos[dimensionIndices[1]];
+        const localA = mod(a, Constants.BUCKET_WIDTH);
+        const localB = mod(b, Constants.BUCKET_WIDTH);
+        labelMap[localA * Constants.BUCKET_WIDTH + localB] = 1;
+      }
+
+      const numberOfSlices = 1;
+      applyVoxelMap(
+        currentLabeledVoxelMap,
+        cube,
+        segmentId,
+        (x, y, out) => {
+          out[0] = x;
+          out[1] = y;
+          out[2] = w;
+        },
+        numberOfSlices,
+        thirdDim,
+        true,
+        0,
+      );
+
+      const thirdDimensionOfSlice = w * labeledMag[thirdDim];
+
+      applyLabeledVoxelMapToAllMissingMags(
+        currentLabeledVoxelMap,
+        labeledZoomStep,
+        dimensionIndices,
+        magInfo,
+        cube,
+        segmentId,
+        thirdDimensionOfSlice,
+        true,
+        0,
+      );
+    }
+
+    Store.dispatch(
+      updateSegmentAction(
+        segmentId,
+        {
+          somePosition: globalPositionsMag1[0],
+          someAdditionalCoordinates: additionalCoordinates || undefined,
+        },
+        volumeTracing.tracingId,
       ),
     );
-    segmentationLayer.cube.pushQueue.push();
+
+    Store.dispatch(finishAnnotationStrokeAction(volumeTracing.tracingId));
   }
 
   /**
@@ -2741,7 +2837,8 @@ class UtilsApi {
   /**
    * Wait for some milliseconds before continuing the control flow.
    *
-   * @example // Wait for 5 seconds
+   * @example
+   * // Wait for 5 seconds
    * await api.utils.sleep(5000);
    */
   sleep(milliseconds: number): Promise<void> {
@@ -2756,7 +2853,8 @@ class UtilsApi {
    * @param {string} type - Can be one of the following: "info", "warning", "success" or "error"
    * @param {string} message - The message string you want to show
    * @param {number} timeout - Time period in milliseconds after which the toast will be hidden. Time is measured as soon as the user moves the mouse. A value of 0 means that the toast will only hide by clicking on it's X button.
-   * @example // Show a toast for 5 seconds
+   * @example
+   * // Show a toast for 5 seconds
    * const removeToast = api.utils.showToast("info", "You just got toasted", false, 5000);
    * // ... optionally:
    * // removeToast();
diff --git a/frontend/javascripts/viewer/api/wk_dev.ts b/frontend/javascripts/viewer/api/wk_dev.ts
index 211c40633e1..4c78549496b 100644
--- a/frontend/javascripts/viewer/api/wk_dev.ts
+++ b/frontend/javascripts/viewer/api/wk_dev.ts
@@ -4,7 +4,7 @@ import { V3 } from "libs/mjs";
 import { roundTo, sleep } from "libs/utils";
 import _ from "lodash";
 import { type OrthoView, OrthoViews, type Vector3 } from "viewer/constants";
-import { Store } from "viewer/singletons";
+import { Model, Store } from "viewer/singletons";
 import type { ApiInterface } from "./api_latest";
 import type ApiLoader from "./api_loader";
 
@@ -59,6 +59,11 @@ export default class WkDev {
     return Store;
   }
 
+  public get model() {
+    /* Access to the model */
+    return Model;
+  }
+
   public get api() {
     /* Access to the API (will fail if API is not initialized yet). */
     if (this._api == null) {
diff --git a/frontend/javascripts/viewer/constants.ts b/frontend/javascripts/viewer/constants.ts
index 3d341a73337..c5738981884 100644
--- a/frontend/javascripts/viewer/constants.ts
+++ b/frontend/javascripts/viewer/constants.ts
@@ -20,8 +20,8 @@ export type NestedMatrix4 = [Vector4, Vector4, Vector4, Vector4]; // Represents
 // For 3D data BucketAddress = x, y, z, mag
 // For higher dimensional data, BucketAddress = x, y, z, mag, [{name: "t", value: t}, ...]
 export type BucketAddress =
-  | Vector4
-  | [number, number, number, number, AdditionalCoordinate[] | null];
+  | readonly [number, number, number, number]
+  | readonly [number, number, number, number, AdditionalCoordinate[] | null];
 
 export type Point2 = {
   x: number;
@@ -49,11 +49,11 @@ export type Rect = {
   height: number;
 };
 export const AnnotationContentTypes = ["skeleton", "volume", "hybrid"];
-export const Vector2Indicies = [0, 1];
-export const Vector3Indicies = [0, 1, 2];
-export const Vector4Indicies = [0, 1, 2, 3];
-export const Vector5Indicies = [0, 1, 2, 3, 4];
-export const Vector6Indicies = [0, 1, 2, 3, 4, 5];
+export const Vector2Indicies = [0, 1] as const;
+export const Vector3Indicies = [0, 1, 2] as const;
+export const Vector4Indicies = [0, 1, 2, 3] as const;
+export const Vector5Indicies = [0, 1, 2, 3, 4] as const;
+export const Vector6Indicies = [0, 1, 2, 3, 4, 5] as const;
 export enum OrthoViews {
   PLANE_XY = "PLANE_XY",
   PLANE_YZ = "PLANE_YZ",
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 33446ab82b7..32de78b782b 100644
--- a/frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts
+++ b/frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts
@@ -2,26 +2,22 @@ import { V3 } from "libs/mjs";
 import { map3, mod } from "libs/utils";
 import _ from "lodash";
 import type { BoundingBoxType, OrthoView, Vector2, Vector3, Vector4 } from "viewer/constants";
-import constants, { Vector3Indicies } from "viewer/constants";
+import constants from "viewer/constants";
 import type { BoundingBoxObject } from "viewer/store";
 import Dimensions from "../dimensions";
-import type { MagInfo } from "../helpers/mag_info";
 
 class BoundingBox {
+  // Min is including, max is excluding
   min: Vector3;
   max: Vector3;
 
   constructor(boundingBox: BoundingBoxType | null | undefined) {
-    // Min is including
-    this.min = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
-    // Max is excluding
-    this.max = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
-
-    if (boundingBox != null) {
-      for (const i of Vector3Indicies) {
-        this.min[i] = Math.max(this.min[i], boundingBox.min[i]);
-        this.max[i] = Math.min(this.max[i], boundingBox.max[i]);
-      }
+    if (boundingBox == null) {
+      this.min = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
+      this.max = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
+    } else {
+      this.min = boundingBox.min.slice() as Vector3;
+      this.max = boundingBox.max.slice() as Vector3;
     }
   }
 
@@ -42,34 +38,33 @@ class BoundingBox {
     return [u, v];
   }
 
-  getBoxForZoomStep = _.memoize((mag: Vector3): BoundingBoxType => {
-    // No `map` for performance reasons
-    const min = [0, 0, 0] as Vector3;
-    const max = [0, 0, 0] as Vector3;
-
-    for (let i = 0; i < 3; i++) {
-      const divisor = constants.BUCKET_WIDTH * mag[i];
-      min[i] = Math.floor(this.min[i] / divisor);
-      max[i] = Math.ceil(this.max[i] / divisor);
-    }
-
-    return {
-      min,
-      max,
-    };
-  });
+  static fromBucketAddress(address: Vector4, mag: Vector3): BoundingBox {
+    return new BoundingBox(this.fromBucketAddressFast(address, mag));
+  }
 
-  containsBucket([x, y, z, zoomStep]: Vector4, magInfo: MagInfo): boolean {
-    /* Checks whether a bucket is contained in the active bounding box.
-     * If the passed magInfo does not contain the passed zoomStep, this method
-     * returns false.
+  static fromBucketAddressFast(
+    [x, y, z, _zoomStep]: Vector4,
+    mag: Vector3,
+  ): { min: Vector3; max: Vector3 } {
+    /*
+     The fast variant does not allocate a Bounding Box instance which can be helpful for tight loops.
      */
-    const magIndex = magInfo.getMagByIndex(zoomStep);
-    if (magIndex == null) {
-      return false;
-    }
-    const { min, max } = this.getBoxForZoomStep(magIndex);
-    return min[0] <= x && x < max[0] && min[1] <= y && y < max[1] && min[2] <= z && z < max[2];
+    const bucketSize = constants.BUCKET_WIDTH;
+
+    // Precompute scaled sizes once
+    const sx = bucketSize * mag[0];
+    const sy = bucketSize * mag[1];
+    const sz = bucketSize * mag[2];
+
+    // Bucket bounds in world space
+    const bxMin = x * sx;
+    const byMin = y * sy;
+    const bzMin = z * sz;
+    const bxMax = bxMin + sx;
+    const byMax = byMin + sy;
+    const bzMax = bzMin + sz;
+
+    return { min: [bxMin, byMin, bzMin], max: [bxMax, byMax, bzMax] };
   }
 
   containsPoint(vec3: Vector3) {
@@ -87,6 +82,13 @@ class BoundingBox {
   }
 
   intersectedWith(other: BoundingBox): BoundingBox {
+    return new BoundingBox(this.intersectedWithFast(other));
+  }
+
+  intersectedWithFast(other: { min: Vector3; max: Vector3 }): { min: Vector3; max: Vector3 } {
+    /*
+     The fast variant does not allocate a Bounding Box instance which can be helpful for tight loops.
+     */
     const newMin = V3.max(this.min, other.min);
     const uncheckedMax = V3.min(this.max, other.max);
 
@@ -94,10 +96,10 @@ class BoundingBox {
     // extent.
     const newMax = V3.max(newMin, uncheckedMax);
 
-    return new BoundingBox({
+    return {
       min: newMin,
       max: newMax,
-    });
+    };
   }
 
   extend(other: BoundingBox): BoundingBox {
diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts b/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts
index 990ddb984cd..f3128793b5d 100644
--- a/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts
+++ b/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts
@@ -16,6 +16,7 @@ import type TemporalBucketManager from "viewer/model/bucket_data_handling/tempor
 import { bucketPositionToGlobalAddress } from "viewer/model/helpers/position_converter";
 import Store from "viewer/store";
 import { getActiveMagIndexForLayer } from "../accessors/flycam_accessor";
+import Dimensions from "../dimensions";
 import { getConstructorForElementClass, uint8ToTypedBuffer } from "../helpers/typed_buffer";
 import BucketSnapshot, { type PendingOperation } from "./bucket_snapshot";
 
@@ -76,6 +77,21 @@ export function markVolumeTransactionEnd() {
   bucketsAlreadyInUndoState.clear();
 }
 
+export type SomeContainment =
+  | { type: "full" }
+  | {
+      type: "partial";
+      // min is inclusive
+      min: Vector3;
+      // max is exclusive
+      max: Vector3;
+    };
+export type Containment =
+  | SomeContainment
+  | {
+      type: "no";
+    };
+
 export class DataBucket {
   readonly type = "data" as const;
   readonly elementClass: ElementClass;
@@ -112,6 +128,7 @@ export class DataBucket {
     elementClass: ElementClass,
     zoomedAddress: BucketAddress,
     temporalBucketManager: TemporalBucketManager,
+    public containment: SomeContainment,
     cube: DataCube,
   ) {
     this.emitter = createNanoEvents();
@@ -454,7 +471,7 @@ export class DataBucket {
     voxelMap: Uint8Array,
     segmentId: number,
     get3DAddress: (arg0: number, arg1: number, arg2: Vector3 | Float32Array) => void,
-    sliceCount: number,
+    sliceOffset: number,
     thirdDimensionIndex: 0 | 1 | 2, // If shouldOverwrite is false, a voxel is only overwritten if
     // its old value is equal to overwritableValue.
     shouldOverwrite: boolean = true,
@@ -473,7 +490,7 @@ export class DataBucket {
           voxelMap,
           segmentId,
           get3DAddress,
-          sliceCount,
+          sliceOffset,
           thirdDimensionIndex,
           shouldOverwrite,
           overwritableValue,
@@ -486,7 +503,7 @@ export class DataBucket {
       voxelMap,
       segmentId,
       get3DAddress,
-      sliceCount,
+      sliceOffset,
       thirdDimensionIndex,
       shouldOverwrite,
       overwritableValue,
@@ -498,8 +515,9 @@ export class DataBucket {
     voxelMap: Uint8Array,
     uncastSegmentId: number,
     get3DAddress: (arg0: number, arg1: number, arg2: Vector3 | Float32Array) => void,
-    sliceCount: number,
-    thirdDimensionIndex: 0 | 1 | 2, // If shouldOverwrite is false, a voxel is only overwritten if
+    sliceOffset: number,
+    thirdDimensionIndex: 0 | 1 | 2,
+    // If shouldOverwrite is false, a voxel is only overwritten if
     // its old value is equal to overwritableValue.
     shouldOverwrite: boolean = true,
     overwritableValue: number = 0,
@@ -509,13 +527,42 @@ export class DataBucket {
 
     const segmentId = castForArrayType(uncastSegmentId, data);
 
-    for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
-      for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
+    const limits = {
+      u: { min: 0, max: Constants.BUCKET_WIDTH },
+      v: { min: 0, max: Constants.BUCKET_WIDTH },
+      w: { min: 0, max: Constants.BUCKET_WIDTH },
+    };
+
+    if (this.containment.type === "partial") {
+      const plane = Dimensions.planeForThirdDimension(thirdDimensionIndex);
+      const [u, v, w] = Dimensions.getIndices(plane);
+      limits.u.min = this.containment.min[u];
+      limits.u.max = this.containment.max[u];
+
+      limits.v.min = this.containment.min[v];
+      limits.v.max = this.containment.max[v];
+
+      limits.w.min = this.containment.min[w];
+      limits.w.max = this.containment.max[w];
+    }
+
+    for (let firstDim = limits.u.min; firstDim < limits.u.max; firstDim++) {
+      for (let secondDim = limits.v.min; secondDim < limits.v.max; secondDim++) {
         if (voxelMap[firstDim * Constants.BUCKET_WIDTH + secondDim] === 1) {
           get3DAddress(firstDim, secondDim, out);
           const voxelToLabel = out;
           voxelToLabel[thirdDimensionIndex] =
-            (voxelToLabel[thirdDimensionIndex] + sliceCount) % Constants.BUCKET_WIDTH;
+            (voxelToLabel[thirdDimensionIndex] + sliceOffset) % Constants.BUCKET_WIDTH;
+
+          if (
+            // The is-partial check is only done as a performance improvement.
+            this.containment.type === "partial" &&
+            (voxelToLabel[thirdDimensionIndex] < limits.w.min ||
+              voxelToLabel[thirdDimensionIndex] >= limits.w.max)
+          ) {
+            continue;
+          }
+
           // The voxelToLabel is already within the bucket and in the correct magnification.
           const voxelAddress = this.cube.getVoxelIndexByVoxelOffset(voxelToLabel);
           const currentSegmentId = Number(data[voxelAddress]);
diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts b/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts
index 0a904d708eb..47b833dce9e 100644
--- a/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts
+++ b/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts
@@ -1,11 +1,12 @@
 import ErrorHandling from "libs/error_handling";
-import { V3 } from "libs/mjs";
+import { V3, V4 } from "libs/mjs";
 import type { ProgressCallback } from "libs/progress_callback";
 import Toast from "libs/toast";
 import {
   areBoundingBoxesOverlappingOrTouching,
   castForArrayType,
   isNumberMap,
+  mod,
   union,
 } from "libs/utils";
 import _ from "lodash";
@@ -18,12 +19,18 @@ import type {
   BucketAddress,
   LabelMasksByBucketAndW,
   Vector3,
+  Vector4,
 } from "viewer/constants";
 import constants, { MappingStatusEnum } from "viewer/constants";
+import Constants from "viewer/constants";
 import { getMappingInfo } from "viewer/model/accessors/dataset_accessor";
 import { getSomeTracing } from "viewer/model/accessors/tracing_accessor";
 import BoundingBox from "viewer/model/bucket_data_handling/bounding_box";
-import type { Bucket } from "viewer/model/bucket_data_handling/bucket";
+import type {
+  Bucket,
+  Containment,
+  SomeContainment,
+} from "viewer/model/bucket_data_handling/bucket";
 import { DataBucket, NULL_BUCKET, NullBucket } from "viewer/model/bucket_data_handling/bucket";
 import type PullQueue from "viewer/model/bucket_data_handling/pullqueue";
 import type PushQueue from "viewer/model/bucket_data_handling/pushqueue";
@@ -47,7 +54,7 @@ const warnAboutTooManyAllocations = _.once(() => {
 });
 
 class CubeEntry {
-  data: Map<number, Bucket>;
+  data: Map<number, DataBucket>;
   boundary: Vector3;
 
   constructor(boundary: Vector3) {
@@ -68,6 +75,27 @@ class CubeEntry {
 const FLOODFILL_VOXEL_THRESHOLD = 5 * 1000000;
 const USE_FLOODFILL_VOXEL_THRESHOLD = false;
 
+const NoContainment = { type: "no" } as const;
+const FullContainment = { type: "full" } as const;
+
+const zeroToBucketWidth = (el: number) => {
+  return el !== 0 ? el : Constants.BUCKET_WIDTH;
+};
+const makeLocalMin = (min: Vector3, mag: Vector3): Vector3 => {
+  return [
+    mod(Math.floor(min[0] / mag[0]), Constants.BUCKET_WIDTH),
+    mod(Math.floor(min[1] / mag[1]), Constants.BUCKET_WIDTH),
+    mod(Math.floor(min[2] / mag[2]), Constants.BUCKET_WIDTH),
+  ];
+};
+const makeLocalMax = (max: Vector3, mag: Vector3): Vector3 => {
+  return [
+    zeroToBucketWidth(mod(Math.ceil(max[0] / mag[0]), Constants.BUCKET_WIDTH)),
+    zeroToBucketWidth(mod(Math.ceil(max[1] / mag[1]), Constants.BUCKET_WIDTH)),
+    zeroToBucketWidth(mod(Math.ceil(max[2] / mag[2]), Constants.BUCKET_WIDTH)),
+  ];
+};
+
 class DataCube {
   BUCKET_COUNT_SOFT_LIMIT = constants.MAXIMUM_BUCKET_COUNT_PER_LAYER;
   buckets: Array<DataBucket>;
@@ -206,27 +234,60 @@ class DataCube {
   }
 
   private getCubeKey(zoomStep: number, allCoords: AdditionalCoordinate[] | undefined | null) {
-    const relevantCoords = (allCoords ?? []).filter(
-      (coord) => this.additionalAxes[coord.name] != null,
-    );
+    if (allCoords == null) {
+      // Instead of defaulting from null to [] for allCoords, we early-out with this simple
+      // return value for performance reasons.
+      return `${zoomStep}`;
+    }
+    const relevantCoords = allCoords.filter((coord) => this.additionalAxes[coord.name] != null);
     return [zoomStep, ...relevantCoords.map((el) => el.value)].join("-");
   }
 
-  isWithinBounds([x, y, z, zoomStep, coords]: BucketAddress): boolean {
+  private checkContainment([x, y, z, zoomStep, coords]: BucketAddress): Containment {
     const cube = this.getOrCreateCubeEntry(zoomStep, coords);
     if (cube == null) {
-      return false;
+      return NoContainment;
     }
 
-    return this.boundingBox.containsBucket([x, y, z, zoomStep], this.magInfo);
+    const mag = this.magInfo.getMagByIndex(zoomStep);
+    if (mag == null) {
+      return NoContainment;
+    }
+
+    const bucketBBox = BoundingBox.fromBucketAddressFast([x, y, z, zoomStep], mag);
+    if (bucketBBox == null) {
+      return NoContainment;
+    }
+
+    const intersectionBBox = this.boundingBox.intersectedWithFast(bucketBBox);
+
+    if (
+      intersectionBBox.min[0] === intersectionBBox.max[0] ||
+      intersectionBBox.min[1] === intersectionBBox.max[1] ||
+      intersectionBBox.min[2] === intersectionBBox.max[2]
+    ) {
+      return NoContainment;
+    }
+    if (
+      V3.equals(intersectionBBox.min, bucketBBox.min) &&
+      V3.equals(intersectionBBox.max, bucketBBox.max)
+    ) {
+      return FullContainment;
+    }
+
+    const { min, max } = intersectionBBox;
+
+    return {
+      type: "partial",
+      min: makeLocalMin(min, mag),
+      max: makeLocalMax(max, mag),
+    };
   }
 
   getBucketIndexAndCube([x, y, z, zoomStep, coords]: BucketAddress): [
     number | null | undefined,
     CubeEntry | null,
   ] {
-    // Removed for performance reasons
-    // ErrorHandling.assert(this.isWithinBounds([x, y, z, zoomStep]));
     const cube = this.getOrCreateCubeEntry(zoomStep, coords);
 
     if (cube != null) {
@@ -272,31 +333,36 @@ class DataCube {
   // NULL_BUCKET if the bucket cannot possibly exist, e.g. because it is
   // outside the dataset's bounding box.
   getOrCreateBucket(address: BucketAddress): Bucket {
-    if (!this.isWithinBounds(address)) {
-      return this.getNullBucket();
-    }
-
-    let bucket = this.getBucket(address, true);
+    let bucket = this.getBucket(address);
 
     if (bucket instanceof NullBucket) {
-      bucket = this.createBucket(address);
+      const containment = this.checkContainment(address);
+      if (containment.type === "no") {
+        return this.getNullBucket();
+      }
+      bucket = this.createBucket(address, containment);
     }
 
     return bucket;
   }
 
   // Returns the Bucket object if it exists, or NULL_BUCKET otherwise.
-  getBucket(address: BucketAddress, skipBoundsCheck: boolean = false): Bucket {
-    if (!skipBoundsCheck && !this.isWithinBounds(address)) {
-      return this.getNullBucket();
-    }
-
+  getBucket(address: BucketAddress): Bucket {
     const [bucketIndex, cube] = this.getBucketIndexAndCube(address);
 
     if (bucketIndex != null && cube != null) {
       const bucket = cube.data.get(bucketIndex);
 
-      if (bucket != null) {
+      // We double-check that the address of the bucket matches the requested
+      // address. If the address is outside of the layer's bbox, the linearization
+      // of the address into one index might collide with another bucket (that is
+      // within the bbox) which is why the check is necessary.
+      // We use slice to ignore the additional coordinates (this is mostly done
+      // to ignore annoying cases like null vs [] which have identical semantics).
+      if (
+        bucket != null &&
+        V4.isEqual(address.slice(0, 4) as Vector4, bucket.zoomedAddress.slice(0, 4) as Vector4)
+      ) {
         return bucket;
       }
     }
@@ -304,8 +370,14 @@ class DataCube {
     return this.getNullBucket();
   }
 
-  createBucket(address: BucketAddress): Bucket {
-    const bucket = new DataBucket(this.elementClass, address, this.temporalBucketManager, this);
+  createBucket(address: BucketAddress, containment: SomeContainment): Bucket {
+    const bucket = new DataBucket(
+      this.elementClass,
+      address,
+      this.temporalBucketManager,
+      containment,
+      this,
+    );
     this.addBucketToGarbageCollection(bucket);
     const [bucketIndex, cube] = this.getBucketIndexAndCube(address);
 
@@ -456,33 +528,6 @@ class DataCube {
     }
   }
 
-  async _labelVoxelInAllResolutions_DEPRECATED(
-    voxel: Vector3,
-    additionalCoordinates: AdditionalCoordinate[] | null,
-    label: number,
-    activeSegmentId?: number | null | undefined,
-  ): Promise<void> {
-    // This function is only provided for the wK front-end api and should not be used internally,
-    // since it only operates on one voxel and therefore is not performance-optimized.
-    // Please make use of a LabeledVoxelsMap instead.
-    const promises = [];
-
-    for (const [magIndex] of this.magInfo.getMagsWithIndices()) {
-      promises.push(
-        this._labelVoxelInResolution_DEPRECATED(
-          voxel,
-          additionalCoordinates,
-          label,
-          magIndex,
-          activeSegmentId,
-        ),
-      );
-    }
-
-    await Promise.all(promises);
-    this.triggerPushQueue();
-  }
-
   async _labelVoxelInResolution_DEPRECATED(
     voxel: Vector3,
     additionalCoordinates: AdditionalCoordinate[] | null,
@@ -490,6 +535,10 @@ class DataCube {
     zoomStep: number,
     activeSegmentId: number | null | undefined,
   ): Promise<void> {
+    // This function is only provided for testing purposes and should not be used internally,
+    // since it only operates on one voxel and therefore is not performance-optimized. It should
+    // be refactored away.
+    // Please make use of a LabeledVoxelsMap instead.
     const voxelInCube = this.boundingBox.containsPoint(voxel);
 
     if (voxelInCube) {
diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts b/frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts
index 67cd7564b42..59664db0c5a 100644
--- a/frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts
+++ b/frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts
@@ -31,6 +31,7 @@ class PullQueue {
   private abortController: AbortController;
   private consecutiveErrorCount: number;
   private isRetryScheduled: boolean;
+  private isDestroyed: boolean = false;
 
   constructor(cube: DataCube, layerName: string, datastoreInfo: DataStoreInfo) {
     this.cube = cube;
@@ -105,6 +106,9 @@ class PullQueue {
         }
       }
     } catch (error) {
+      if (this.isDestroyed) {
+        return;
+      }
       for (const bucketAddress of batch) {
         const bucket = this.cube.getBucket(bucketAddress);
 
@@ -197,6 +201,12 @@ class PullQueue {
       this.priorityQueue.queue(el);
     }
   }
+
+  destroy() {
+    this.isDestroyed = true;
+    this.clear();
+    this.abortRequests();
+  }
 }
 
 export default PullQueue;
diff --git a/frontend/javascripts/viewer/model/reducers/reducer_helpers.ts b/frontend/javascripts/viewer/model/reducers/reducer_helpers.ts
index 8486bb7d7f0..073efc73f94 100644
--- a/frontend/javascripts/viewer/model/reducers/reducer_helpers.ts
+++ b/frontend/javascripts/viewer/model/reducers/reducer_helpers.ts
@@ -74,7 +74,6 @@ export function convertFrontendBoundingBoxToServer(
   };
 }
 
-// Currently unused.
 export function convertPointToVecInBoundingBox(boundingBox: ServerBoundingBox): BoundingBoxObject {
   return {
     width: boundingBox.width,
diff --git a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts
index d6f02dbb193..f5d4ea3cae4 100644
--- a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts
+++ b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts
@@ -175,6 +175,9 @@ function handleRemoveSegment(state: WebknossosState, action: RemoveSegmentAction
 function handleUpdateSegment(state: WebknossosState, action: UpdateSegmentAction) {
   return updateSegments(state, action.layerName, (segments) => {
     const { segmentId, segment } = action;
+    if (segmentId === 0) {
+      return segments;
+    }
     const oldSegment = segments.getNullable(segmentId);
 
     let somePosition;
diff --git a/frontend/javascripts/viewer/model/volumetracing/volume_annotation_sampling.ts b/frontend/javascripts/viewer/model/volumetracing/volume_annotation_sampling.ts
index 9217c92b349..e41f4e87b3b 100644
--- a/frontend/javascripts/viewer/model/volumetracing/volume_annotation_sampling.ts
+++ b/frontend/javascripts/viewer/model/volumetracing/volume_annotation_sampling.ts
@@ -1,6 +1,7 @@
 import { map3 } from "libs/utils";
 import _ from "lodash";
 import messages from "messages";
+import type { Writeable } from "types/globals";
 import type { BucketAddress, LabeledVoxelsMap, Vector3 } from "viewer/constants";
 import constants from "viewer/constants";
 import type { Bucket } from "viewer/model/bucket_data_handling/bucket";
@@ -340,12 +341,13 @@ export function applyVoxelMap(
     get3DAddress(0, 0, out);
     const thirdDimensionValueInBucket = out[2];
 
-    for (let sliceCount = 0; sliceCount < numberOfSlicesToApply; sliceCount++) {
-      const newThirdDimValue = thirdDimensionValueInBucket + sliceCount;
+    for (let sliceOffset = 0; sliceOffset < numberOfSlicesToApply; sliceOffset++) {
+      const newThirdDimValue = thirdDimensionValueInBucket + sliceOffset;
 
-      if (sliceCount > 0 && newThirdDimValue % constants.BUCKET_WIDTH === 0) {
+      if (sliceOffset > 0 && newThirdDimValue % constants.BUCKET_WIDTH === 0) {
         // The current slice is in the next bucket in the third direction.
-        const nextBucketZoomedAddress: BucketAddress = [...labeledBucketZoomedAddress];
+        const nextBucketZoomedAddress: Writeable<BucketAddress> = [...labeledBucketZoomedAddress];
+
         nextBucketZoomedAddress[thirdDimensionIndex]++;
         postprocessBucket(bucket);
         bucket = dataCube.getOrCreateBucket(nextBucketZoomedAddress);
@@ -361,7 +363,7 @@ export function applyVoxelMap(
           voxelMap,
           segmentId,
           get3DAddress,
-          sliceCount,
+          sliceOffset,
           thirdDimensionIndex,
           shouldOverwrite,
           overwritableValue,
diff --git a/frontend/javascripts/viewer/model_initialization.ts b/frontend/javascripts/viewer/model_initialization.ts
index 9e3359a1d23..3e1c500bdc9 100644
--- a/frontend/javascripts/viewer/model_initialization.ts
+++ b/frontend/javascripts/viewer/model_initialization.ts
@@ -46,7 +46,6 @@ import UrlManager, {
 import {
   determineAllowedModes,
   getDataLayers,
-  getDatasetBoundingBox,
   getDatasetCenter,
   getLayerByName,
   getSegmentationLayerByName,
@@ -118,6 +117,7 @@ import Store from "viewer/store";
 import { doAllLayersHaveTheSameRotation } from "./model/accessors/dataset_layer_transformation_accessor";
 import { setVersionNumberAction } from "./model/actions/save_actions";
 import {
+  convertPointToVecInBoundingBox,
   convertServerAdditionalAxesToFrontEnd,
   convertServerAnnotationToFrontendAnnotation,
 } from "./model/reducers/reducer_helpers";
@@ -580,7 +580,6 @@ function getMergedDataLayersFromDatasetAndVolumeTracings(
     );
 
     const fallbackLayer = fallbackLayerIndex > -1 ? originalLayers[fallbackLayerIndex] : null;
-    const boundingBox = getDatasetBoundingBox(dataset).asServerBoundingBox();
     const mags = tracing.mags || [];
     const tracingHasMagList = mags.length > 0;
     let coordinateTransformsMaybe = {};
@@ -606,7 +605,7 @@ function getMergedDataLayersFromDatasetAndVolumeTracings(
       elementClass: tracing.elementClass,
       category: "segmentation",
       largestSegmentId: tracing.largestSegmentId,
-      boundingBox,
+      boundingBox: convertPointToVecInBoundingBox(tracing.boundingBox),
       resolutions: tracingMags,
       mappings:
         fallbackLayer != null && "mappings" in fallbackLayer ? fallbackLayer.mappings : undefined,


Reviews:
user: login: philippotto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T14:41:40Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 8**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/shaders/coords.glsl.ts (1)</summary><blockquote>

`71-72`: _⚠️ Potential issue_

**Remove temporary debug code**

This code forces `isOutsideOfBoundingBox` to always return `false`, which would bypass all bounding box checking. The "todop: remove again" comment confirms this is temporary debugging code that shouldn't be committed.


Please remove these lines to restore the proper bounding box checking functionality:

```diff
- // todop: remove again
- return false;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (12)</summary><blockquote>

<details>
<summary>CHANGELOG.unreleased.md (1)</summary><blockquote>

`27-27`: **Clear and accurate changelog entry.**

This entry correctly summarizes the fix that prevents voxels outside the layer bounding box from being brushed, matching the PR objectives.

Minor stylistic suggestion: Consider "Voxels outside the layer bounding box" instead of "outside of".


```diff
-Voxels outside of the layer bounding box cannot be brushed, anymore. [#8602](https://github.com/scalableminds/webknossos/pull/8602)
+Voxels outside the layer bounding box cannot be brushed anymore. [#8602](https://github.com/scalableminds/webknossos/pull/8602)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~27-~27: This phrase is redundant. Consider using “outside”.
Context: ...bleminds/webknossos/pull/8599) - Voxels outside of the layer bounding box cannot be brushe...

(OUTSIDE_OF)

</details>

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts (1)</summary><blockquote>

`178-180`: **Prevent updating segment ID 0**

This guard clause prevents updates to segment ID 0, which is a critical part of the PR's objective to respect boundary constraints during voxel labeling operations.


Consider adding a short comment explaining why segment ID 0 needs special handling:

```diff
 if (segmentId === 0) {
+  // Skip updates for segment ID 0 as it represents background/empty space
   return segments;
 }
```

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts (1)</summary><blockquote>

`346-352`: **Shallow-copy of `BucketAddress` can still leak mutations into the original array**

`[...labeledBucketZoomedAddress]` copies the tuple only one level deep.  
If the 5-th element (the `AdditionalCoordinate[]`) is not `null`, the inner
array is shared between the original and `nextBucketZoomedAddress`. While you
currently only increment the numeric part (`nextBucketZoomedAddress[thirdDimensionIndex]++`),
future modifications (e.g. pushing an additional coordinate) would silently
mutate the original object and break the readonly guarantee you have just
introduced.

```diff
-const nextBucketZoomedAddress: Writeable<BucketAddress> = [...labeledBucketZoomedAddress];
+const nextBucketZoomedAddress: Writeable<BucketAddress> = [
+  ...labeledBucketZoomedAddress.slice(0, 4),
+  labeledBucketZoomedAddress[4] != null
+    ? [...labeledBucketZoomedAddress[4]] // deep-copy the additional-coordinate list
+    : null,
+] as Writeable<BucketAddress>;
```

This keeps the immutable promise of `BucketAddress` intact and prevents
accidental side effects.

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/constants.ts (1)</summary><blockquote>

`52-56`: **Spelling: `Vector*Indicies` → `Vector*Indices`**

Tiny nit – “indices” is the correct plural of “index”.

```diff
-export const Vector4Indicies = [0, 1, 2, 3] as const;
+export const Vector4Indices = [0, 1, 2, 3] as const;
```

(The same applies to the other constant arrays for consistency.)

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (2)</summary><blockquote>

`125-131`: **Document the new `containment` constructor parameter**

The extra parameter is non-obvious for callers skimming the signature.
Adding a short JSDoc snippet (expected coordinate space, inclusive/exclusive
semantics) will spare future confusion when creating buckets in other
modules/tests.

---

`528-546`: **Iteration bounds are recomputed for every voxel-write**

`Dimensions.planeForThirdDimension()` and `Dimensions.getIndices()` are called
for every `applyVoxelMap` invocation.  For heavy labeling operations this can
be a hot path.  Caching the `(u,v,w)` index triple once outside the per-bucket
loop (or even on the bucket instance) would avoid repeated allocations and
string → enum conversions.

</blockquote></details>
<details>
<summary>frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts (2)</summary><blockquote>

`67-68`: **Avoid hard-coding dataset bounds in tests**

`cubeBoundingBox` is currently defined with magic numbers `[1, 2, 3] … [1023, …]`.  
If `Constants.BUCKET_WIDTH` or the dataset size changes, the tests may silently become invalid.  
Consider deriving the bounds from `Constants.BUCKET_WIDTH` or `cube.getSize()` instead, e.g.

```diff
-const cubeBoundingBox = new BoundingBox({ min: [1, 2, 3], max: [1023, 1024, 1025] });
+const max = Constants.BUCKET_WIDTH * 32;          // or cube.getSize()
+const cubeBoundingBox = new BoundingBox({ min: [1, 2, 3], max: [max, max + 1, max + 2] });
```

---

`667-678`: **Reuse BoundingBox helpers instead of manual range checks**

The expected–data loop manually checks whether each `(firstDim, secondDim)` lies inside the bounding box:

```ts
if (firstDim < cubeBoundingBox.min[0] || …) { return; }
```

This duplicates logic that already exists in `BoundingBox.containsPoint`.  
Using the helper clarifies intent and prevents off-by-one mistakes:

```diff
- if (
-   firstDim < cubeBoundingBox.min[0] ||
-   secondDim < cubeBoundingBox.min[1] ||
-   firstDim >= cubeBoundingBox.max[0] ||
-   secondDim >= cubeBoundingBox.max[1]
- ) {
-   return;
- }
+ if (!cubeBoundingBox.containsPoint([firstDim, secondDim, Z])) return;
```

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts (1)</summary><blockquote>

`88-103`: **Explicit “empty” intersection would improve downstream checks**

`intersectedWithFast` collapses non-overlapping boxes to a zero-volume
box (`min == max`).  Call-sites that only test equality might
misinterpret this as a “real” box.  Consider returning `null` (or an
`isEmpty` flag) to make empty intersections explicit and avoid accidental
processing of degenerate boxes.

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)</summary><blockquote>

`73-92`: **Local-coordinate helpers look good but can be micro-simplified**

The helper functions achieve the desired mapping from global → local bucket coordinates, handling the “32 instead of 0” edge case.  Two small improvements:

1. The `zeroToBucketWidth` lambda is recreated for every import of this file. Consider an inline ternary to avoid an extra closure entirely.
2. `mod(Math.floor(x / mag[i]), Constants.BUCKET_WIDTH)` is executed three times per call. When `mag` is guaranteed to be positive, using plain remainder (`%`) is sufficient and ~20 % faster.

These are micro-optimisations—feel free to ignore if readability is preferred.

---

`351-358`: **Address collision guard: good idea, but `_.isEqual` is overkill**

Using a defensive check to avoid collisions when linearising addresses
is 👍.  However, `_.isEqual` incurs an extra allocation and deep-compare.

A manual compare of four integers is ~8× faster and clearer:

```diff
-      if (bucket != null && _.isEqual(address.slice(0, 4), bucket.zoomedAddress.slice(0, 4))) {
+      if (
+        bucket != null &&
+        address[0] === bucket.zoomedAddress[0] &&
+        address[1] === bucket.zoomedAddress[1] &&
+        address[2] === bucket.zoomedAddress[2] &&
+        address[3] === bucket.zoomedAddress[3]
+      ) {
```

---

`530-534`: **Deprecation notice is helpful—consider adding a runtime warning**

The explanatory comment is great.  To make accidental production usage
even less likely, emit a `console.warn` if `NODE_ENV !== "production"`
when this method is invoked.

```diff
-    // This function is only provided for testing purposes …
+    if (process.env.NODE_ENV !== "production") {
+      console.warn(
+        "_labelVoxelInResolution_DEPRECATED is for tests only and will be removed soon.",
+      );
+    }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 228f681034369d549ff104fc7683fc8b2573f5e3 and aa12cc579eb176551132e6fd7b865155f25c679b.

</details>

<details>
<summary>📒 Files selected for processing (20)</summary>

* `CHANGELOG.unreleased.md` (1 hunks)
* `frontend/javascripts/libs/utils.ts` (1 hunks)
* `frontend/javascripts/oxalis/api/api_latest.ts` (12 hunks)
* `frontend/javascripts/oxalis/api/wk_dev.ts` (2 hunks)
* `frontend/javascripts/oxalis/constants.ts` (2 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts` (3 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts` (8 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts` (7 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/pullqueue.ts` (3 hunks)
* `frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts` (1 hunks)
* `frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts` (3 hunks)
* `frontend/javascripts/oxalis/shaders/coords.glsl.ts` (1 hunks)
* `frontend/javascripts/test/helpers/apiHelpers.ts` (1 hunks)
* `frontend/javascripts/test/model/binary/cube.spec.ts` (2 hunks)
* `frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts` (1 hunks)
* `frontend/javascripts/test/model/binary/pullqueue.spec.ts` (1 hunks)
* `frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts` (4 hunks)
* `frontend/javascripts/test/model/texture_bucket_manager.spec.ts` (1 hunks)
* `frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts` (6 hunks)
* `frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (9)</summary>

<details>
<summary>frontend/javascripts/test/model/binary/pullqueue.spec.ts (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)</summary>

* `DataBucket` (93-833)

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)</summary>

* `DataBucket` (93-833)

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts (2)</summary><blockquote>

<details>
<summary>frontend/javascripts/libs/utils.ts (1)</summary>

* `Writeable` (18-18)

</details>
<details>
<summary>frontend/javascripts/oxalis/constants.ts (1)</summary>

* `BucketAddress` (22-24)

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/test/model/binary/cube.spec.ts (2)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/constants.ts (1)</summary>

* `Vector3` (13-13)

</details>
<details>
<summary>frontend/javascripts/oxalis/model/helpers/mag_info.ts (1)</summary>

* `MagInfo` (11-225)

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/api/wk_dev.ts (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/singletons.ts (1)</summary>

* `Model` (7-7)

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts (2)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/constants.ts (3)</summary>

* `Vector3` (13-13)
* `BoundingBoxType` (41-44)
* `Vector4` (14-14)

</details>
<details>
<summary>frontend/javascripts/libs/mjs.ts (1)</summary>

* `V3` (398-398)

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/constants.ts (1)</summary>

* `Vector3` (13-13)

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (3)</summary>

* `DataBucket` (93-833)
* `Containment` (80-91)
* `Bucket` (70-70)

</details>
<details>
<summary>frontend/javascripts/oxalis/constants.ts (2)</summary>

* `Vector3` (13-13)
* `BucketAddress` (22-24)

</details>
<details>
<summary>frontend/javascripts/libs/utils.ts (1)</summary>

* `mod` (24-26)

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/constants.ts (1)</summary><blockquote>

<details>
<summary>webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala (2)</summary>

* `AdditionalCoordinate` (90-95)
* `AdditionalCoordinate` (97-112)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>CHANGELOG.unreleased.md</summary>

[style] ~27-~27: This phrase is redundant. Consider using “outside”.
Context: ...bleminds/webknossos/pull/8599) - Voxels outside of the layer bounding box cannot be brushe...

(OUTSIDE_OF)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: backend-tests
* GitHub Check: build-smoketest-push

</details>

<details>
<summary>🔇 Additional comments (19)</summary><blockquote>

<details>
<summary>frontend/javascripts/libs/utils.ts (1)</summary>

`18-18`: **Great utility type addition!**

This is a well-implemented TypeScript utility type that removes the `readonly` modifier from properties. It will be useful for type-safe operations that need to temporarily modify otherwise readonly types.

</details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/pullqueue.ts (3)</summary>

`34-34`: **LGTM: Added property for tracking destruction state.**

This boolean flag will help manage the lifecycle of the PullQueue instance.

---

`109-111`: **Good error handling improvement.**

Early returning when the queue is destroyed prevents unnecessary error processing after the instance has been terminated, avoiding potential side effects.

---

`205-209`: **Well-designed destroy method.**

This method provides a clean way to terminate the queue's operations by:
1. Setting the destroyed flag to prevent further operations
2. Clearing the queue (while preserving highest priority items)
3. Aborting any ongoing requests

This is a good pattern for resource cleanup.

</details>
<details>
<summary>frontend/javascripts/test/model/texture_bucket_manager.spec.ts (1)</summary>

`38-38`: **LGTM: Updated DataBucket constructor with containment type**

The addition of `{ type: "full" }` correctly updates the test to align with the new `DataBucket` constructor signature that supports containment types, which is essential for the bounding box containment feature being implemented in this PR.

</details>
<details>
<summary>frontend/javascripts/test/helpers/apiHelpers.ts (1)</summary>

`193-193`: **Code cleanup: Using consolidated destroy() method**

Good refactoring to replace separate `clear()` and `abortRequests()` calls with a single `destroy()` method, improving code maintainability.

</details>
<details>
<summary>frontend/javascripts/oxalis/api/wk_dev.ts (2)</summary>

`7-7`: **LGTM: Updated import to include Model**

Appropriate addition of Model to the import from singletons to support the new getter.

---

`62-65`: **Added model getter for debugging support**

This addition provides a convenient way to access the Model singleton for debugging purposes, complementing the existing store getter. Since this class is explicitly for debugging and not an official API, this is a reasonable addition.

</details>
<details>
<summary>frontend/javascripts/test/model/binary/pullqueue.spec.ts (1)</summary>

`86-87`: **Test code correctly updated for new DataBucket constructor signature**

The test now properly includes the new `containment` parameter with `{ type: "full" }` when instantiating DataBucket objects, which aligns with the PR's goal of making buckets respect bounding box containment.

</details>
<details>
<summary>frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts (1)</summary>

`56-57`: **Test code correctly updated for new DataBucket constructor signature**

All DataBucket instantiations have been properly updated to include the new `containment` parameter with `{ type: "full" }`, conforming to the updated constructor signature. These changes maintain test functionality while supporting the PR's goal of bounding box containment.



Also applies to: 63-64, 73-74, 84-85, 96-97

</details>
<details>
<summary>frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts (1)</summary>

`253-259`: **Test code correctly updated for new DataBucket constructor signature**

Both DataBucket instantiations have been properly updated to include the new `containment` parameter with `{ type: "full" }`, conforming to the updated constructor signature. These changes maintain test functionality while supporting the PR's goal of bounding box containment.



Also applies to: 264-270

</details>
<details>
<summary>frontend/javascripts/test/model/binary/cube.spec.ts (1)</summary>

`88-92`: **Update assertion helpers to reflect removed “bounds-check” flag**

The removed `true` argument means `getBucket` always enforces the
bbox-containment internally.  Make sure helper assertions (e.g. tests that
expect an out-of-bbox bucket to be `null`) are still valid under the new
behaviour; otherwise the tests could turn into false positives.

</details>
<details>
<summary>frontend/javascripts/oxalis/constants.ts (1)</summary>

`23-24`: **👍  Read-only `BucketAddress` dramatically improves type-safety**

Making the tuple immutable forces callers to be explicit (via `Writeable`)
when they really need to mutate the address. Nice improvement!

</details>
<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts (2)</summary>

`2215-2233`: **Potential XY-indexing mix-up when setting bits in `labelMap`**

`labelMap[localA * BUCKET_WIDTH + localB] = 1` assumes
row-major `(x, y)` layout, but `applyVoxelMap` internally treats its map
as `(y, x)` (column-major).  If that is the case, labels are rotated by
90°.  Please double-check the expected ordering and swap the indices if
necessary:

```diff
-labelMap[localA * Constants.BUCKET_WIDTH + localB] = 1;
+labelMap[localB * Constants.BUCKET_WIDTH + localA] = 1; // if column-major
```

---

`2248-2263`: **`numberOfSlices` hard-coded to 1**

`applyVoxelMap` receives `numberOfSlices = 1`, yet the function is
executed inside a loop that already fixes a single `w` layer.  If the
voxel map ever spans more than one XY slice inside the same bucket,
those additional slices will be silently dropped.  Either compute the
actual slice count or document the limitation.

</details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)</summary>

`241-280`: **Containment logic is correct; watch for negative coordinates**

Great job replacing the boolean “inside?” check with the more expressive
`Containment` tri-state.  The fast-path exits cover 99 % of calls.

Two things to keep in mind:

1. `BoundingBox.fromBucketAddressFast` and `makeLocal*` assume the global
   min/max can be divided by `mag` without rounding surprises.  If your
   dataset ever contains negative coordinates, `Math.floor` behaves
   differently from integer division. (Your `mod` helper keeps the result
   non-negative, which is good.)
2. This method allocates three `BoundingBox` instances per call
   (`bucketBBox`, `intersectionBBox`, the result of `intersectedWithFast`).
   If profiling shows this is hot, consider an in-place intersection to
   reuse objects.

Overall: ✅ functional, ❓ potential perf if called in tight loops.

---

`366-372`: **Constructor signature change lacks type-safety downstream**

`createBucket` now forwards `containment` to `DataBucket`, but the
call-sites in the rest of the file haven’t been updated to pass a typed
`Containment` object when *directly* instantiating `DataBucket`
(only this helper does).  
Search the repo for `new DataBucket(`; any external instantiation must
be adapted, otherwise TypeScript will happily accept `containment:
any`.

Recommend adding

```ts
type NewDataBucketParams = ConstructorParameters<typeof DataBucket>;
```

and exporting it, to help catching wrong calls during compilation.

---

`231-239`: _⚠️ Potential issue_

**`getCubeKey` produces two keys for the same semantics (`null` vs `[]`)**

When `allCoords` is `null` you return `"${zoomStep}"`,  
but when it is an *empty array* you return `"${zoomStep}-"`.

Both represent “no additional coordinates” yet create two distinct map
entries, leaking memory and duplicating buckets.

```diff
-    const relevantCoords = allCoords.filter((coord) => this.additionalAxes[coord.name] != null);
-    return [zoomStep, ...relevantCoords.map((el) => el.value)].join("-");
+    const relevantCoords = allCoords.filter(
+      (coord) => this.additionalAxes[coord.name] != null,
+    );
+    if (relevantCoords.length === 0) {
+      return `${zoomStep}`;
+    }
+    return [zoomStep, ...relevantCoords.map((el) => el.value)].join("-");
```




> Likely an incorrect or invalid review comment.

</details>
<details>
<summary>frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts (1)</summary>

`451-451`: **Formatting alignment only.** This change simply realigns the closing comment delimiter to match the indentation of the opening delimiter. No functional impact.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:53:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)</summary><blockquote>

`536-565`: _🛠️ Refactor suggestion_

**Missing runtime safeguard for buckets outside the layer bbox**

While the code handles the "partial" case correctly, there's no explicit check for the `"no"` containment type.


Add an explicit early-return to avoid accidental writes when a bucket is completely outside the layer's bounding box:

```diff
+if (this.containment.type === "no") {
+  return false; // bucket fully outside bbox – nothing to do
+}
```

This ensures voxels will never be modified in buckets that should be excluded entirely.

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (1)</summary><blockquote>

`20-21`: **Duplicate default import from the same module**

`constants` is already imported as the default export on line 20. Importing the default again under the alias `Constants` on line 21 works syntactically, but:
• it adds bytes to every bundle that includes this file, because the bundler deduplicates the module *after* symbol mangling,  
• it can easily confuse future readers ("are `constants` and `Constants` different?").



Prefer a single import and reuse the same identifier:

```diff
-import constants, { MappingStatusEnum } from "oxalis/constants";
-import Constants from "oxalis/constants";
+import constants, { MappingStatusEnum } from "oxalis/constants";
/* if you need an alias, do:
+const Constants = constants;
+*/
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)</summary><blockquote>

`549-565`: **Missing optimization for out-of-bounds slices**

When `sliceOffset` is completely outside the w-dimension limits for a partially contained bucket, the function still iterates through all U×V positions only to skip them later.


Consider adding an early-return optimization to skip processing slices that are entirely outside the allowed W-range:

```diff
+if (
+  this.containment.type === "partial" &&
+  (sliceOffset < limits.w.min || sliceOffset >= limits.w.max)
+) {
+  return false;
+}
```

This would save O(32²) operations per slice, which is especially beneficial for thin bbox slabs.

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts (2)</summary><blockquote>

`2190-2192`: **Explicit `null` additionalCoordinates are ignored**

```ts
const additionalCoordinates =
  optAdditionalCoordinates === undefined
    ? state.flycam.additionalCoordinates
    : optAdditionalCoordinates;
```

Passing `null` explicitly is indistinguishable from passing `undefined`; both fall back to the fly-cam coordinates. This prevents a caller from explicitly requesting "no additional coordinates".


Consider checking specifically for `undefined` rather than using the nullish coalescing operator:

```diff
-const additionalCoordinates =
-  optAdditionalCoordinates === undefined
-    ? state.flycam.additionalCoordinates
-    : optAdditionalCoordinates;
+const additionalCoordinates = 
+  optAdditionalCoordinates === undefined
+    ? state.flycam.additionalCoordinates
+    : optAdditionalCoordinates;
```

---

`2237-2266`: **Inefficient per-slice `applyVoxelMap` calls**

The loop runs `applyVoxelMap` and the expensive `applyLabeledVoxelMapToAllMissingMags` once per Z slice. For large brushes affecting many slices, this scales poorly (O(slices * buckets)).


Consider building one `LabeledVoxelsMap` for the entire call and invoking the functions only once to avoid repeated traversal and queueing.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between aa12cc579eb176551132e6fd7b865155f25c679b and fd87641fa21d57213fc916b68f941453ab4e8f43.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `frontend/javascripts/oxalis/api/api_latest.ts` (12 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts` (3 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts` (8 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts` (7 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/constants.ts (1)</summary>

* `Vector3` (13-13)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: backend-tests
* GitHub Check: build-smoketest-push

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (3)</summary>

`80-93`: **Well-designed containment type system for bucket handling**

The new `SomeContainment` and `Containment` types provide a clean representation of how buckets intersect with the layer's bounding box. The distinction between "no", "full", and "partial" containment with inclusive min and exclusive max bounds is a solid design.

---

`131-131`: **Added containment parameter to DataBucket constructor**

The constructor now accepts a containment parameter to store information about how the bucket intersects with the layer's bounding box, which will be used when modifying voxels.

---

`530-547`: **Proper handling of partial containment limits**

The implementation correctly sets up dimension-specific limits for u, v, and w coordinates when dealing with partial containment. This ensures that operations respect the layer's bounding box.

</details>
<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts (1)</summary>

`2182-2280`: **Significant improvement in voxel labeling approach**

The rewrite of `labelVoxels` shifts from per-voxel labeling to a much more efficient batch approach. The code now:
1. Groups voxels by their third dimension coordinate
2. Constructs labeled voxel maps for each bucket
3. Applies those maps using proper containment-aware functions

This implementation properly respects layer bounding boxes and should be more performant.

</details>
<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (4)</summary>

`77-96`: **Well-designed helper functions for containment calculations**

The constants and helper functions for creating local min/max coordinates are well-designed and help encapsulate the logic needed for partial containment. The `makeLocalMin` and `makeLocalMax` functions handle the tricky modulo calculations cleanly.

---

`245-284`: **Enhanced containment checking replaces simple boolean check**

The new `checkContainment` method provides much more detailed information than a boolean check:
- Identifies if a bucket is completely outside the bounding box
- Determines if a bucket is fully contained in the bounding box
- For partial containment, calculates the exact intersection coordinates

This enables more precise voxel operations that respect the layer's boundaries.

---

`334-346`: **Simplified bucket creation with containment awareness**

The `getOrCreateBucket` method now uses the detailed containment information to decide whether to create a bucket, improving upon the previous approach. This integrates well with the new containment-aware functionality.

---

`527-562`: **Clear documentation for deprecated method**

The comment for `_labelVoxelInResolution_DEPRECATED` now clarifies that it should only be used for testing purposes and suggests using the more efficient `LabeledVoxelsMap` approach instead. This helps guide developers toward the correct pattern.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T15:44:38Z
user: login: philippotto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T06:17:08Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T06:17:33Z
user: login: MichaelBuessemeyer
body: Nice, thanks for this PR and also improving performance :tada: (at least that how I understand parts of this PR).

Codewise I did not really find anything. I just left a few suggestions.

Testing:
Everything worked as expected except for one small detail: During testing with restricted mags (4-4-1) the area where drawing is allowed is a little off by -1 in x and y direction (did not test z direction). Please see the image: The gray DS border is not clearly where the turquoise border (my brushing) is. When no mag restriction was in place, everything worked fine :D
![image](https://github.com/user-attachments/assets/ed6b4891-6827-4dd8-a6de-116b368d77ad)

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T12:49:04Z
user: login: philippotto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T12:53:48Z
user: login: philippotto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T12:56:18Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (4)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (1)</summary><blockquote>

`21-22`: **Duplicate default import from the same module**

`constants` is already imported as the default export on line 21. Importing the default again under the alias `Constants` on line 22 is redundant.



```diff
-import constants, { MappingStatusEnum } from "oxalis/constants";
-import Constants from "oxalis/constants";
+import constants, { MappingStatusEnum } from "oxalis/constants";
+const Constants = constants;
```

</blockquote></details>
<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts (3)</summary><blockquote>

`2213-2237`: **Label application is performed per slice**

The labelVoxels implementation processes voxels grouped by their third dimension (z), creating and applying a separate voxel map for each slice. This approach works well for small brushes but could be inefficient for large brushes affecting many slices.



Consider building one complete `LabeledVoxelsMap` for the entire operation and invoking `applyVoxelMap` and `applyLabeledVoxelMapToAllMissingMags` just once to avoid repeated traversal and queueing, especially when labeling large volumes with many slices:

```diff
-for (const group of Object.values(groupedByW)) {
-  const w = group[0][thirdDim];
-  const currentLabeledVoxelMap: LabeledVoxelsMap = new Map();
-
-  for (const pos of group) {
-    // Processing for each position in the group
-  }
-
-  // Apply for this slice...
-  applyVoxelMap(...);
-  applyLabeledVoxelMapToAllMissingMags(...);
-}
+// Build one complete map
+const completeLabeledVoxelMap: LabeledVoxelsMap = new Map();
+
+for (const group of Object.values(groupedByW)) {
+  const w = group[0][thirdDim];
+  
+  for (const pos of group) {
+    // Add to the complete map...
+  }
+}
+
+// Apply once for all slices
+applyVoxelMap(completeLabeledVoxelMap, ...);
+applyLabeledVoxelMapToAllMissingMags(completeLabeledVoxelMap, ...);
```

---

`2246-2246`: **Use `w` to avoid conversion**

```typescript
const w = parseInt(thirdDim);
```

This conversion is unnecessary since `group[0][thirdDim]` would already give you the same value.



```diff
-const w = parseInt(thirdDim);
+const w = group[0][thirdDim];
```

---

`2190-2192`: _🛠️ Refactor suggestion_

**Handling of optional additionalCoordinates could be improved**

The current implementation:
```typescript
const additionalCoordinates =
  optAdditionalCoordinates === undefined
    ? state.flycam.additionalCoordinates
    : optAdditionalCoordinates;
```

should distinguish between `undefined` and `null` values explicitly, as passing `null` intentionally is indistinguishable from passing `undefined`.



```diff
-const additionalCoordinates =
-  optAdditionalCoordinates === undefined
-    ? state.flycam.additionalCoordinates
-    : optAdditionalCoordinates;
+const additionalCoordinates =
+  optAdditionalCoordinates === undefined
+    ? state.flycam.additionalCoordinates
+    : optAdditionalCoordinates;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts (1)</summary><blockquote>

`2270-2270`: **Consider renaming 'sliceOffset' for clarity**

The variable name `numberOfSlices` (used as the 5th argument to `applyVoxelMap`) could be renamed to something more descriptive like `sliceOffset` or `sliceCount` to better reflect its purpose in the function.


```diff
-const numberOfSlices = 1;
+const sliceOffset = 1;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fd87641fa21d57213fc916b68f941453ab4e8f43 and a41777c89822355dc9da6db6d3f07b439c039588.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `frontend/javascripts/libs/utils.ts` (1 hunks)
* `frontend/javascripts/oxalis/api/api_latest.ts` (12 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts` (7 hunks)
* `frontend/javascripts/types/globals.d.ts` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* frontend/javascripts/libs/utils.ts
* frontend/javascripts/types/globals.d.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (5)</summary>

* `DataBucket` (95-835)
* `Containment` (89-93)
* `NullBucket` (49-67)
* `Bucket` (70-70)
* `SomeContainment` (80-88)

</details>
<details>
<summary>frontend/javascripts/oxalis/constants.ts (3)</summary>

* `Vector3` (13-13)
* `BucketAddress` (22-24)
* `Vector4` (14-14)

</details>
<details>
<summary>frontend/javascripts/libs/utils.ts (1)</summary>

* `mod` (22-24)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: frontend-tests</summary>

<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts</summary>

[failure] 16-16: 
Module '"libs/utils"' has no exported member 'Writeable'.

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: backend-tests
* GitHub Check: build-smoketest-push

</details>

<details>
<summary>🔇 Additional comments (16)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (12)</summary>

`2-2`: **Updated vector utility imports to include V4**

The import was updated to include V4 utilities, which are used for comparing 4D vectors in the bucket address comparison logic later in the code.

---

`9-9`: **Added `mod` import from utils**

The `mod` utility function is now imported for handling bucket coordinate calculations correctly, especially important for partial bucket containment.

---

`19-19`: **Added `Vector4` type import**

The `Vector4` type is now imported to properly type the zoomed addresses when comparing bucket addresses.

---

`26-30`: **Improved type imports from the bucket module**

The type imports have been expanded to include `Containment` and `SomeContainment` from the bucket module, which are essential for the new containment-aware functionality.

---

`57-57`: **Updated data type from array to Map for better performance**

The `data` property in `CubeEntry` was changed from an array to a `Map<number, DataBucket>`, which is more efficient for bucket lookups by index.

---

`78-80`: **Added containment type constants**

Added `NoContainment` and `FullContainment` constants that represent the two basic containment types. These improve code readability and prevent repeatedly creating the same objects.

---

`81-97`: **Added helpers for bucket coordinate calculations**

New helper functions have been added to manage containment-aware bucket coordinates:
1. `zeroToBucketWidth` converts 0 to `BUCKET_WIDTH`, handling edge cases
2. `makeLocalMin` calculates local minimum coordinates with modulo arithmetic
3. `makeLocalMax` calculates local maximum coordinates with proper bucket width handling

These functions support the partial containment calculation in the new `checkContainment` method.

---

`236-285`: **Implemented containment-aware bucket checking**

The new `checkContainment` method replaces the simple boolean check with detailed containment checking. It determines whether a bucket:
1. Has no overlap with the bounding box (`"no"`)
2. Is fully contained within the bounding box (`"full"`)
3. Is partially contained by the bounding box (`"partial"`)

For partial containment, it also computes the local min/max coordinates that define the valid portion of the bucket. This is crucial for restricting voxel operations to only the valid parts of partially contained buckets.

---

`350-371`: **Improved bucket address validation in `getBucket`**

The `getBucket` method now double-checks that the bucket's stored address matches the requested address using `V4.isEqual` to avoid collisions due to address linearization. This ensures that operations on out-of-bounds addresses don't affect other buckets accidentally.

---

`336-347`: **Updated containment check in `getOrCreateBucket`**

The method now uses the new `checkContainment` function instead of a simple bounds check. It only creates buckets for addresses that have at least partial containment, returning `NULL_BUCKET` for addresses with no containment.

---

`373-389`: **Updated bucket creation to include containment information**

The `createBucket` method now accepts and passes a `containment` parameter to the `DataBucket` constructor, allowing buckets to know their containment status relative to the layer's bounding box. This enables buckets to restrict voxel operations to only the valid portions.

---

`531-566`: **Clarified deprecation of `_labelVoxelInResolution_DEPRECATED`**

The method was updated with a clearer comment explaining that it should only be used for testing and should be replaced with the more efficient approach using `LabeledVoxelsMap`. This aligns with the refactoring of `labelVoxels` in api_latest.ts.

</details>
<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts (4)</summary>

`24-24`: **Added LabeledVoxelsMap type import**

This type is crucial for the refactored `labelVoxels` method which now uses a map of labeled voxels for batch processing.

---

`73-73`: **Added enforceActiveVolumeTracing import**

The new implementation of `labelVoxels` needs to enforce that an active volume tracing exists, which is handled by this imported function.

---

`136-136`: **Added imports for new voxel labeling functionality**

Several new imports have been added to support the refactored `labelVoxels` method:
- `finishAnnotationStrokeAction` to properly finish the annotation stroke
- `Dimensions` for dimensional calculations
- `zoomedPositionToZoomedAddress` for position-to-address conversion
- `applyLabeledVoxelMapToAllMissingMags` and `applyVoxelMap` for efficient voxel application

These imports provide the functionality needed for the containment-aware voxel labeling approach.



Also applies to: 145-145, 155-156, 160-161

---

`2171-2280`: **Complete rewrite of labelVoxels method**

The `labelVoxels` method has been completely refactored from an asynchronous per-voxel approach to a synchronous batch processing approach. Key improvements:

1. Uses `enforceActiveVolumeTracing` to verify an active volume tracing
2. Groups voxels by their third dimension (z-slice) for efficient processing
3. Creates containment-aware labeled voxel maps for each bucket
4. Applies voxel maps respecting bucket containment boundaries
5. Propagates changes across magnifications
6. Dispatches appropriate actions to update the UI and finish the stroke

This implementation aligns with the PR's objective to ensure voxels outside the layer's bounding box cannot be mutated, as the containment-aware bucket system will restrict operations to valid regions.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:00:12Z
user: login: philippotto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:00:27Z
user: login: philippotto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:01:39Z
user: login: philippotto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:03:30Z
user: login: philippotto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:07:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (1)</summary><blockquote>

`21-22`: _🛠️ Refactor suggestion_

**Remove duplicate import of Constants**

You're importing the default export from 'oxalis/constants' twice - once as `constants` and again as `Constants`. This is redundant and can confuse readers.


```diff
import constants, { MappingStatusEnum } from "oxalis/constants";
-import Constants from "oxalis/constants";
```

Then use `constants` instead of `Constants` throughout the code, or rename the first import:

```diff
-import constants, { MappingStatusEnum } from "oxalis/constants";
-import Constants from "oxalis/constants";
+import Constants, { MappingStatusEnum } from "oxalis/constants";
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (1)</summary><blockquote>

`2-2`: **Minor optimization: Import specific symbols from 'libs/mjs'**

Instead of importing the entire `V3` and `V4` modules, consider importing only the specific functions you need to reduce bundle size.

```diff
-import { V3, V4 } from "libs/mjs";
+import { isEqual as V4isEqual, equals as V3equals, scale3, sub, add } from "libs/mjs";
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a41777c89822355dc9da6db6d3f07b439c039588 and 049693883a172c97783161a95a5fa9b27e859b65.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `frontend/javascripts/oxalis/api/api_latest.ts` (12 hunks)
* `frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts` (7 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (4)</summary>

* `DataBucket` (95-835)
* `Containment` (89-93)
* `Bucket` (70-70)
* `SomeContainment` (80-88)

</details>
<details>
<summary>frontend/javascripts/oxalis/constants.ts (3)</summary>

* `Vector3` (13-13)
* `BucketAddress` (22-24)
* `Vector4` (14-14)

</details>
<details>
<summary>frontend/javascripts/libs/utils.ts (1)</summary>

* `mod` (22-24)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: frontend-tests</summary>

<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts</summary>

[failure] 2009-2009: 
Cannot find name 'Writeable'.

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build-smoketest-push
* GitHub Check: backend-tests

</details>

<details>
<summary>🔇 Additional comments (18)</summary><blockquote>

<details>
<summary>frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (9)</summary>

`9-9`: **Good improvement: Adding the `mod` utility function for proper modulo operations**

This addition is a great improvement as JavaScript's default `%` operator doesn't handle negative numbers as mathematical modulo. The imported `mod` utility ensures consistent behavior with both positive and negative inputs.

---

`26-30`: **Good type imports for containment handling**

The addition of these types makes the code more maintainable by providing clear type definitions for bucket containment states.

---

`78-79`: **Good use of `const` declarations for containment types**

Using const objects with the `as const` assertion for the containment types ensures proper type inference and makes the code more maintainable.

---

`81-97`: **Well-implemented helper functions for bounding box calculations**

These utility functions abstract complex logic for determining local bucket coordinates. The `zeroToBucketWidth` function correctly handles the edge case where the modulo result is 0.

---

`246-285`: **Excellent implementation of detailed containment checking**

This new `checkContainment` method improves on the previous boolean logic by providing detailed information about how a bucket intersects with the layer's bounding box. This is crucial for the PR's goal of limiting voxel modification to areas within the bounding box.

The implementation correctly identifies three distinct states:
1. No containment (`NoContainment`)
2. Full containment (`FullContainment`)
3. Partial containment with specific min/max coordinates

---

`336-344`: **Improved bucket creation logic with containment awareness**

The updated `getOrCreateBucket` now properly checks containment status before creating a bucket, and passes the containment information to the bucket creation process. This ensures buckets are properly constrained to the bounding box.

---

`350-371`: **Enhanced `getBucket` with improved address validation**

The updated implementation adds an important check to verify that the bucket's stored address matches the requested address. This prevents potential issues when linearizing out-of-bounds addresses might collide with valid addresses.

Using `V4.isEqual` for the comparison is efficient and clear.

---

`373-381`: **Updated bucket creation with containment awareness**

The `createBucket` method now accepts and passes along the containment information, allowing buckets to know exactly which portions of their volume are within the layer's bounding box.

---

`531-566`: **Clarified purpose of deprecated method**

The comment explaining that this method is only for testing and should be refactored away is valuable. It guides future developers to use the newer, more efficient approach with `LabeledVoxelsMap`.

</details>
<details>
<summary>frontend/javascripts/oxalis/api/api_latest.ts (9)</summary>

`16-16`: **Good dependency update: Importing specific utilities**

Importing only the needed utilities `coalesce` and `mod` is a clean approach that improves code maintainability.

---

`24-24`: **Good dependency update: Adding LabeledVoxelsMap type**

Adding this type import supports the refactored labelVoxels implementation that uses labeled voxel maps.

---

`73-73`: **Useful addition of enforceActiveVolumeTracing**

Importing this utility function enables better validation and error handling in the refactored labelVoxels method.

---

`136-136`: **Good addition of finishAnnotationStrokeAction**

This import supports the stroke-based labeling approach in the refactored implementation.

---

`145-145`: **Good import: Using named import for Dimensions**

Using a named import for Dimensions rather than lowercase `dimensions` improves consistency.

---

`155-155`: **Additional position converter import**

Adding the `zoomedPositionToZoomedAddress` import is necessary for the refactored labelVoxels method.

---

`160-161`: **Good imports for volume annotation functionality**

These imports provide the necessary functions for the refactored labelVoxels implementation, leveraging the updated containment-aware bucket handling.

---

`422-436`: **Improved example formatting and clarity**

The updated example comments are more consistently formatted, which improves the API documentation. The examples now follow a more consistent pattern.

---

`2171-2280`: **Excellent refactoring of the labelVoxels method**

The completely rewritten `labelVoxels` method is a significant improvement over the previous implementation:

1. It operates synchronously rather than asynchronously
2. It processes all input voxels as a single brush stroke
3. It groups voxels by the third dimension for efficient processing
4. It creates labeled voxel maps and applies them to the segmentation cube
5. It leverages the new containment-aware bucket handling to respect bounding box limits

This implementation aligns with the PR's goal of ensuring voxel labeling only happens within the layer's bounding box.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:26:32Z
user: login: MichaelBuessemeyer
body: > Therefore, I think, that it is alright to let the user brush a bit over the bbox limit as otherwise, there would be an area within the bbox which cannot be labeled. I had to adapt the rounding code so that it also works similarily for annotating at the bottom-right border. See my latest commit :)

Sounds good as well as your latest changes.

Once the CI is green and this branch is up to date with the master I'll give this another quick re-test and then it should be ready to go :green_circle: 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:17:02Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts (1)</summary><blockquote>

`205-209`: **Good consolidation of cleanup operations.**

The new `destroy()` method properly combines existing cleanup operations (`clear()` and `abortRequests()`) into a single method, ensuring they're always performed together. This supports the PR's overall goal of improving bucket and data management.


Consider adding a JSDoc comment to document the purpose and behavior of this method:

```diff
+/**
+ * Destroys this pull queue by clearing it and aborting all pending requests.
+ * After calling this method, the queue should no longer be used.
+ */
 destroy() {
   this.isDestroyed = true;
   this.clear();
   this.abortRequests();
 }
```

</blockquote></details>
<details>
<summary>CHANGELOG.unreleased.md (1)</summary><blockquote>

`34-34`: **LGTM: Good changelog entry for the bounding box fix.**

The changelog entry clearly describes the bugfix related to preventing voxel brushing outside of layer bounding boxes. This aligns well with the PR objectives.


You could consider simplifying "outside of" to just "outside" for conciseness:

```diff
- Voxels outside of the layer bounding box cannot be brushed, anymore. [#8602](https://github.com/scalableminds/webknossos/pull/8602)
+ Voxels outside the layer bounding box cannot be brushed, anymore. [#8602](https://github.com/scalableminds/webknossos/pull/8602)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~34-~34: This phrase is redundant. Consider using “outside”.
Context: ...bleminds/webknossos/pull/8599) - Voxels outside of the layer bounding box cannot be brushe...

(OUTSIDE_OF)

</details>

</details>

</blockquote></details>
<details>
<summary>frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts (1)</summary><blockquote>

`127-133`: **Mandatory `containment` argument may break external instantiations**

`DataBucket` now requires a `SomeContainment` parameter.  All internal
call-sites seem to be updated, but any downstream plugins or test helpers
that directly instantiate `new DataBucket(...)` will now fail at compile
time.

If binary compatibility is important, consider providing a default value
(e.g. `{ type: "full" }`) or exposing a convenience factory to construct a
bucket without having to know the containment details.

</blockquote></details>
<details>
<summary>frontend/javascripts/viewer/api/api_latest.ts (2)</summary><blockquote>

`2205-2213`: **Minor optimisation: skip `mod()` when indices are already in-range**

`a` and `b` originate from `Math.floor(pos[i] / labeledMag[i])`, therefore
they are guaranteed to be `>= 0`.  Using `% Constants.BUCKET_WIDTH`
directly is both clearer and ~15 % faster than an extra function call.

```diff
-const localA = mod(a, Constants.BUCKET_WIDTH);
-const localB = mod(b, Constants.BUCKET_WIDTH);
+const localA = a % Constants.BUCKET_WIDTH;
+const localB = b % Constants.BUCKET_WIDTH;
```

This is obviously a micro-optimisation, but `labelVoxels` is called from
interactive brushing code; every allocation spared helps when thousands of
voxels are processed per frame.

---

`2256-2266`: **Possible performance win: merge identical `Uint8Array` instances**

A new `Uint8Array` is allocated per bucket per slice.  For dense brushes
this can allocate hundreds of short-lived 1 KB buffers each stroke.  You
can reuse a single zeroed array from a pool:

```ts
const pooledLabelMap = new Uint8Array(Constants.BUCKET_WIDTH ** 2);
// …
pooledLabelMap.fill(0);
// write voxels …
currentLabeledVoxelMap.set(bucketZoomedAddress, pooledLabelMap);
```

Allocation pressure drops and GC pauses during heavy painting sessions are
reduced.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 049693883a172c97783161a95a5fa9b27e859b65 and f6a3ff4859eb000ee543ca247993e2f0336d6332.

</details>

<details>
<summary>📒 Files selected for processing (19)</summary>

* `CHANGELOG.unreleased.md` (1 hunks)
* `frontend/javascripts/libs/utils.ts` (1 hunks)
* `frontend/javascripts/test/helpers/apiHelpers.ts` (1 hunks)
* `frontend/javascripts/test/model/binary/cube.spec.ts` (2 hunks)
* `frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts` (1 hunks)
* `frontend/javascripts/test/model/binary/pullqueue.spec.ts` (1 hunks)
* `frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts` (4 hunks)
* `frontend/javascripts/test/model/texture_bucket_manager.spec.ts` (1 hunks)
* `frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts` (7 hunks)
* `frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts` (1 hunks)
* `frontend/javascripts/types/globals.d.ts` (2 hunks)
* `frontend/javascripts/viewer/api/api_latest.ts` (12 hunks)
* `frontend/javascripts/viewer/api/wk_dev.ts` (2 hunks)
* `frontend/javascripts/viewer/constants.ts` (2 hunks)
* `frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts` (3 hunks)
* `frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts` (8 hunks)
* `frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts` (7 hunks)
* `frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts` (3 hunks)
* `frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts
* frontend/javascripts/test/model/binary/pullqueue.spec.ts
* frontend/javascripts/viewer/api/wk_dev.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary>

* frontend/javascripts/libs/utils.ts
* frontend/javascripts/types/globals.d.ts
* frontend/javascripts/test/helpers/apiHelpers.ts
* frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts
* frontend/javascripts/test/model/texture_bucket_manager.spec.ts
* frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts
* frontend/javascripts/test/model/binary/cube.spec.ts
* frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>frontend/javascripts/viewer/api/api_latest.ts (4)</summary><blockquote>

<details>
<summary>frontend/javascripts/types/globals.d.ts (1)</summary>

* `Writeable` (25-25)

</details>
<details>
<summary>frontend/javascripts/types/api_types.ts (1)</summary>

* `AdditionalCoordinate` (30-30)

</details>
<details>
<summary>frontend/javascripts/libs/utils.ts (1)</summary>

* `mod` (22-24)

</details>
<details>
<summary>frontend/javascripts/viewer/model/volumetracing/volume_annotation_sampling.ts (1)</summary>

* `applyVoxelMap` (304-377)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>CHANGELOG.unreleased.md</summary>

[style] ~34-~34: This phrase is redundant. Consider using “outside”.
Context: ...bleminds/webknossos/pull/8599) - Voxels outside of the layer bounding box cannot be brushe...

(OUTSIDE_OF)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: backend-tests
* GitHub Check: build-smoketest-push
* GitHub Check: frontend-tests

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts (2)</summary>

`34-34`: **Good addition of lifecycle tracking flag.**

The `isDestroyed` flag provides a clear way to track the PullQueue's lifecycle state, allowing for proper cleanup and preventing operations after destruction.

---

`109-111`: **Good defensive programming practice.**

Adding an early return when `isDestroyed` is true prevents further processing of errors after the queue has been destroyed, which avoids potential issues with accessing or modifying destroyed objects.

</details>
<details>
<summary>frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts (1)</summary>

`178-180`: **LGTM: Guard clause prevents processing segment ID 0.**

Good addition of a guard clause to prevent processing segment ID 0. This is a safety check that helps maintain data integrity by avoiding updates to invalid segments.

</details>
<details>
<summary>frontend/javascripts/viewer/constants.ts (2)</summary>

`22-24`: **Improve type safety with readonly BucketAddress.**

Adding `readonly` to the tuple types is a good defensive programming practice that prevents accidental mutations of bucket addresses, which helps maintain referential integrity throughout the application.

---

`52-56`: **Improve type safety with const assertions for vector indices.**

Using `as const` with these vector indices arrays ensures type-level immutability and allows TypeScript to infer more precise types (readonly tuples of specific length instead of number[]). This change complements the other immutability improvements in this PR.

</details>
<details>
<summary>frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts (5)</summary>

`5-5`: **Use default import instead of named import for constants.**

This change is correctly importing the constants module as a default import, which matches the export style in constants.ts.

---

`14-21`: **LGTM: Simplified BoundingBox constructor.**

The constructor has been simplified using a cleaner approach with direct assignment instead of iterative updates. This makes the code more readable and likely more efficient.

---

`41-68`: **LGTM: Added efficient bounding box creation from bucket addresses.**

The new static methods `fromBucketAddress` and `fromBucketAddressFast` provide a clean way to create bounding boxes from bucket addresses. The "fast" variant is particularly useful as it avoids unnecessary object allocations in performance-critical code paths.

The calculation of bucket bounds in world space is now more efficient with precomputed scaled sizes.

---

`84-103`: **LGTM: Optimized bounding box intersection with allocation-free variant.**

The refactored `intersectedWith` method and the new `intersectedWithFast` method follow the same pattern of providing both a class-based and an allocation-free variant for flexibility. This is excellent for performance optimization in tight loops where allocation overhead matters.

---

`1-268`: <details>
<summary>✅ Verification successful</summary>

**Verify the impact of removing containsBucket method.**

The `containsBucket` method was removed entirely from the BoundingBox class. Based on the PR objectives, this seems to be replaced by a more detailed containment state system elsewhere that distinguishes between full, partial, or no containment.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for references to the removed containsBucket method
rg "containsBucket" --type ts
```

Length of output: 141

---



</details>

**No production references to `containsBucket` found**  

A search only turned up a mock in a test, not any real calls:  
- frontend/javascripts/test/model/binary/pullqueue.spec.ts: stubbed `containsBucket: vi.fn()`

Since no production code invokes `containsBucket` on `BoundingBox`, its removal has no breaking impact.

</details>
<details>
<summary>frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts (7)</summary>

`78-84`: **Return `Constants.BUCKET_WIDTH` only when strictly needed**

`zeroToBucketWidth` returns `BUCKET_WIDTH` for an input of `0`, but keeps any other value unchanged.  
If the input originates from `mod()` it is always guaranteed to be in `[0, BUCKET_WIDTH-1]`.  
Calling the helper therefore introduces an extra branch per axis that will only ever be taken for the
single (rare) corner case where `mod()` itself returned `0`.

```diff
-const zeroToBucketWidth = (el: number) => {
-  return el !== 0 ? el : Constants.BUCKET_WIDTH;
-};
+const zeroToBucketWidth = (el: number) =>
+  el === 0 ? Constants.BUCKET_WIDTH : el;
```

Very minor, but trimming the function to a single expression avoids the extra conditional and
slightly improves readability.  
[ suggest_nitpick ]

---

`85-97`: **Negative coordinates can leak through `mod()`**

`makeLocalMin/Max` rely on `mod()` to normalise possibly negative values.  
If `BoundingBox.min` can ever become negative (e.g. cropped datasets that are shifted into negative
space) we have to ensure that `mod()` implements a mathematical modulo and not JavaScript’s `%`
remainder (which would keep the sign).  
Otherwise the resulting local coordinates will be negative and violate the invariant
`0 ≤ local < BUCKET_WIDTH`, breaking bucket-internal indexing.

Please double-check `libs/utils/mod`:

```ts
export const mod = (a: number, n: number) => ((a % n) + n) % n;
```

If the helper is not implemented like above, a one-liner fix in `mod()` will protect every call site.  
[ request_verification ]

---

`237-244`: **Early-out path duplicates work & allocates needlessly**

`getCubeKey` short-circuits for `allCoords == null`, but still allocates a new
array via `filter().map()` when a (possibly empty) array is passed.  
Because this method is on the hot path of every bucket access, consider avoiding the two new arrays:

```diff
-const relevantCoords = allCoords.filter((coord) => this.additionalAxes[coord.name] != null);
-return [zoomStep, ...relevantCoords.map((el) => el.value)].join("-");
+if (!allCoords?.length) return `${zoomStep}`;
+let key = `${zoomStep}`;
+for (const c of allCoords) {
+  if (this.additionalAxes[c.name] != null) key += `-${c.value}`;
+}
+return key;
```

This keeps GC pressure lower when the viewer is continuously drawing.  
[ suggest_optional_refactor ]

---

`246-285`: **`checkContainment` does the job, but performs duplicate work & misses a non-overlap guard**

1. `getOrCreateCubeEntry` already performed a `getMagByIndex` lookup.  
   `checkContainment` repeats the same call, costing one extra hashmap read per bucket creation.  
   Capturing the `mag` in `getOrCreateCubeEntry` and returning it alongside the cube would
   eliminate the duplicate lookup.

2. If `BoundingBox.intersectedWithFast()` returns an *invalid* box on non-overlap
   (`min > max` on one axis), the subsequent `intersectionBBox.min === intersectionBBox.max`
   check will not detect it and we will fall through to the *partial* case,
   producing bogus `local min/max`.  
   A rapid early-out improves robustness:

```diff
+if (!areBoundingBoxesOverlappingOrTouching(this.boundingBox, bucketBBox)) {
+  return NoContainment;
+}
```

3. Minor naming: `FullContainment` and `NoContainment` are constants, but
   `SomeContainment` is a type.  Renaming the former to `NO_CONTAINMENT` /
   `FULL_CONTAINMENT` (or vice-versa) makes the code self-explanatory.  
[ suggest_essential_refactor ]

---

`336-345`: **`getOrCreateBucket` can skip `createBucket` for truly empty partials**

When `containment.type === "partial"` we still allocate a full
`DataBucket`, even though the intersected volume might be *entirely empty*
(e.g. brush attempts that miss the bounding box by one voxel).
Checking `makeLocalMin == makeLocalMax` would let us
return `NULL_BUCKET` instead and avoid needless allocations:

```ts
if (
  containment.type === "partial" &&
  V3.equals(containment.min, containment.max)
) {
  return this.getNullBucket();
}
```

This optimisation keeps the bucket count lower when a user draws along the edge of
the dataset.  
[ suggest_optional_refactor ]

---

`356-365`: **Address collision guard is helpful but expensive**

The slice + `V4.isEqual` comparison protects against collisions across different
additional-coordinate cubes, yet runs on every successful lookup.  
Because collisions can only happen for out-of-bbox addresses (which are rare),
consider reversing the logic: compare only when `cubeKey` comes from an *unknown*
cube (i.e. when `cube == null`, the case we already treat separately), or cache a
Boolean `isOutsideBoundingBox` flag on the bucket at construction time and skip
the equality check for `false`.  
[ suggest_optional_refactor ]

---

`538-542`: **Deprecation comment – please add TODO with ticket**

The note clarifies that `_labelVoxelInResolution_DEPRECATED` is test-only and
should be removed, but without a tracking ticket it may linger indefinitely.
Appending a TODO with a link to an issue (or opening one) makes the clean-up
actionable.  
[ suggest_nitpick ]

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T11:34:02Z
user: login: MichaelBuessemeyer
body: Hej I just gave this another test and it doesn't seem to work correctly anymore, or I am testing it wrong :thinking:

Here is a screenshot of my test: 
![image](https://github.com/user-attachments/assets/2233eb70-2f93-42ef-9664-bb58ff9a215c)
I am using l4_sample for testing with ds layer bounds: 
- `3090, 3090, 512, 900, 900, 900` -> segmentation
- `3072, 3072, 512, 1024, 1024, 1024` (original) -> color

As you can see in the screenshot, I am able to draw beyond the segmentation layers bounds and as I understood this PR, this shouldn't be possible.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:02:11Z
user: login: MichaelBuessemeyer
body: Sorry, but I am not sure about the latest change. The type `ServerVolumeTracing` itself should already have a bounding box which should be used as the source of truth for the annotation bbox limit IMO.

https://github.com/scalableminds/webknossos/pull/7580 fixes the behaviour for the volume tracing bbox for explorations. So just using `tracing.boundingBox` in the model_init should be ok here imo.

Sorry for the extra round
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T09:21:50Z
user: login: MichaelBuessemeyer
body: Looks good and works now as expected :tada: :dancers: . Thanks a lot for applying all the feedback.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T12:51:19Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>CHANGELOG.unreleased.md (1)</summary><blockquote>

`35-37`: **Refine changelog entry wording for clarity and consistency.**

The phrase “outside of” is redundant and the comma before “anymore” is unnecessary. Consider rephrasing to match the style of other entries and improve readability:

```diff
- Voxels outside of the layer bounding box cannot be brushed, anymore. [#8602]
+ Voxels outside the layer bounding box can no longer be brushed. [#8602]
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~36-~36: This phrase is redundant. Consider using “outside”.
Context: ...bleminds/webknossos/pull/8599) - Voxels outside of the layer bounding box cannot be brushe...

(OUTSIDE_OF)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c356887feb8b2ceb712a99b5467095acd47cac15 and d59c476e2b6ef11db2aa2b38659f0480264688bc.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `CHANGELOG.unreleased.md` (1 hunks)
* `frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>CHANGELOG.unreleased.md</summary>

[style] ~36-~36: This phrase is redundant. Consider using “outside”.
Context: ...bleminds/webknossos/pull/8599) - Voxels outside of the layer bounding box cannot be brushe...

(OUTSIDE_OF)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: frontend-tests
* GitHub Check: build-smoketest-push
* GitHub Check: backend-tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T12:55:07Z

Review comments:
diff_hunk: @@ -68,6 +68,8 @@ export const getWorldCoordUVW: ShaderModule = {
 export const isOutsideOfBoundingBox: ShaderModule = {
   code: `
     bool isOutsideOfBoundingBox(vec3 worldCoordUVW) {
+      // todop: remove again
path: frontend/javascripts/oxalis/shaders/coords.glsl.ts
user: login: philippotto
body: only used for easier manual testing (will remove once you tested it)
created_at: 2025-05-06T14:39:21Z
author_association: MEMBER
diff_hunk: @@ -361,7 +362,7 @@ export function applyVoxelMap(
           voxelMap,
           segmentId,
           get3DAddress,
-          sliceCount,
+          sliceOffset,
           thirdDimensionIndex,
path: frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Confirm all call-sites were migrated to the new `sliceOffset` parameter**

`applyVoxelMap` no longer accepts `sliceCount`.  The current file is updated
accordingly, but other call-sites (e.g. in legacy helpers or test utilities)
might have been overlooked and will now fail at compile-time.


If the command prints anything, those locations still use the old signature.

---

🏁 Script executed:

```shell
#!/bin/bash
# quick sanity-check:
rg --line-number --fixed-strings "applyVoxelMap(" | grep -v "sliceOffset"
```

Length of output: 1109

---



</details>

**Fix remaining applyVoxelMap call-sites to use the new sliceOffset parameter**

The signature for applyVoxelMap was changed to replace sliceCount with sliceOffset. The following call-sites still use the old signature and will fail to compile:

• frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts  
  – lines 628, 680, 720  
• frontend/javascripts/oxalis/api/api_latest.ts – line 2235  
• frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts – line 361 (bucket.applyVoxelMap)  
• frontend/javascripts/oxalis/model/sagas/volume/helpers.ts – lines 144, 251  
• frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts – line 468  

Please update each call to pass sliceOffset instead of sliceCount (keeping thirdDimensionIndex unchanged). For example:

```diff
- applyVoxelMap(dataMap, cube, dim, get3DAddress, sliceCount, thirdDimensionIndex, clamp);
+ applyVoxelMap(dataMap, cube, dim, get3DAddress, sliceOffset, thirdDimensionIndex, clamp);
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T14:53:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,6 +77,19 @@ export function markVolumeTransactionEnd() {
   bucketsAlreadyInUndoState.clear();
 }
 
+export type Containment =
+  | {
+      type: "no";
+    }
+  | { type: "full" }
+  | {
+      type: "partial";
+      // min is inclusive
+      min: Vector3;
+      // max is exclusive
+      max: Vector3;
+    };
+
path: frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Runtime safeguard for `containment.type === "no"` is missing**

`DataBucket` is now instantiated with a `containment` flag, yet the write
routines only treat the `"partial"` case specially.  If a bucket with
`containment.type === "no"` slipped through, the current implementation would
still label voxels outside the layer bbox.

Consider an explicit early-return to avoid accidental writes:

```diff
if (this.containment.type === "no") {
  return false; // bucket fully outside bbox – nothing to do
}
```



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T14:53:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,13 +525,42 @@ export class DataBucket {
 
     const segmentId = castForArrayType(uncastSegmentId, data);
 
-    for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
-      for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
+    const limits = {
+      u: { min: 0, max: Constants.BUCKET_WIDTH },
+      v: { min: 0, max: Constants.BUCKET_WIDTH },
+      w: { min: 0, max: Constants.BUCKET_WIDTH },
+    };
+
+    if (this.containment.type === "partial") {
+      const plane = Dimensions.planeForThirdDimension(thirdDimensionIndex);
+      const [u, v, w] = Dimensions.getIndices(plane);
+      limits.u.min = this.containment.min[u];
+      limits.u.max = this.containment.max[u];
+
+      limits.v.min = this.containment.min[v];
+      limits.v.max = this.containment.max[v];
+
+      limits.w.min = this.containment.min[w];
+      limits.w.max = this.containment.max[w];
+    }
+
+    for (let firstDim = limits.u.min; firstDim < limits.u.max; firstDim++) {
+      for (let secondDim = limits.v.min; secondDim < limits.v.max; secondDim++) {
         if (voxelMap[firstDim * Constants.BUCKET_WIDTH + secondDim] === 1) {
           get3DAddress(firstDim, secondDim, out);
           const voxelToLabel = out;
           voxelToLabel[thirdDimensionIndex] =
-            (voxelToLabel[thirdDimensionIndex] + sliceCount) % Constants.BUCKET_WIDTH;
+            (voxelToLabel[thirdDimensionIndex] + sliceOffset) % Constants.BUCKET_WIDTH;
+
+          if (
+            // The is-partial check is only done as a performance improvement.
+            this.containment.type === "partial" &&
+            (voxelToLabel[thirdDimensionIndex] < limits.w.min ||
+              voxelToLabel[thirdDimensionIndex] >= limits.w.max)
+          ) {
+            continue;
+          }
+
path: frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fast-path when slice is outside the allowed W-range**

When `sliceOffset` lies completely outside `[limits.w.min, limits.w.max)`, the
function still iterates over **all** U×V positions only to skip them later.
An early continue would save O(32²) work per slice:

```diff
+if (
+  this.containment.type === "partial" &&
+  (sliceOffset < limits.w.min || sliceOffset >= limits.w.max)
+) {
+  return false;
+}
```
The optimisation is especially helpful for thin bbox slabs.



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T14:53:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,34 +38,33 @@ class BoundingBox {
     return [u, v];
   }
 
-  getBoxForZoomStep = _.memoize((mag: Vector3): BoundingBoxType => {
-    // No `map` for performance reasons
-    const min = [0, 0, 0] as Vector3;
-    const max = [0, 0, 0] as Vector3;
-
-    for (let i = 0; i < 3; i++) {
-      const divisor = constants.BUCKET_WIDTH * mag[i];
-      min[i] = Math.floor(this.min[i] / divisor);
-      max[i] = Math.ceil(this.max[i] / divisor);
-    }
-
-    return {
-      min,
-      max,
-    };
-  });
+  static fromBucketAddress(address: Vector4, mag: Vector3): BoundingBox | null {
+    return new BoundingBox(this.fromBucketAddressFast(address, mag));
+  }
path: frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`fromBucketAddress` can no longer return `null`**

`fromBucketAddress` is typed as `BoundingBox | null`, but the current
implementation always instantiates a `BoundingBox`, so `null` is
unreachable.  Either restore the previous sentinel return or update the
signature:

```diff
-static fromBucketAddress(address: Vector4, mag: Vector3): BoundingBox | null {
+static fromBucketAddress(address: Vector4, mag: Vector3): BoundingBox {
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T14:53:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,34 +38,33 @@ class BoundingBox {
     return [u, v];
   }
 
-  getBoxForZoomStep = _.memoize((mag: Vector3): BoundingBoxType => {
-    // No `map` for performance reasons
-    const min = [0, 0, 0] as Vector3;
-    const max = [0, 0, 0] as Vector3;
-
-    for (let i = 0; i < 3; i++) {
-      const divisor = constants.BUCKET_WIDTH * mag[i];
-      min[i] = Math.floor(this.min[i] / divisor);
-      max[i] = Math.ceil(this.max[i] / divisor);
-    }
-
-    return {
-      min,
-      max,
-    };
-  });
+  static fromBucketAddress(address: Vector4, mag: Vector3): BoundingBox | null {
+    return new BoundingBox(this.fromBucketAddressFast(address, mag));
+  }
 
-  containsBucket([x, y, z, zoomStep]: Vector4, magInfo: MagInfo): boolean {
-    /* Checks whether a bucket is contained in the active bounding box.
-     * If the passed magInfo does not contain the passed zoomStep, this method
-     * returns false.
+  static fromBucketAddressFast(
+    [x, y, z, _zoomStep]: Vector4,
+    mag: Vector3,
+  ): { min: Vector3; max: Vector3 } | null {
+    /*
+     The fast variant does not allocate a Bounding Box instance which can be helpful for tight loops.
      */
-    const magIndex = magInfo.getMagByIndex(zoomStep);
-    if (magIndex == null) {
-      return false;
-    }
-    const { min, max } = this.getBoxForZoomStep(magIndex);
-    return min[0] <= x && x < max[0] && min[1] <= y && y < max[1] && min[2] <= z && z < max[2];
+    const bucketSize = constants.BUCKET_WIDTH;
+
+    // Precompute scaled sizes once
+    const sx = bucketSize * mag[0];
+    const sy = bucketSize * mag[1];
+    const sz = bucketSize * mag[2];
+
+    // Bucket bounds in world space
+    const bxMin = x * sx;
+    const byMin = y * sy;
+    const bzMin = z * sz;
+    const bxMax = bxMin + sx;
+    const byMax = byMin + sy;
+    const bzMax = bzMin + sz;
+
+    return { min: [bxMin, byMin, bzMin], max: [bxMax, byMax, bzMax] };
   }
path: frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Inconsistent `null` in `fromBucketAddressFast`**

`fromBucketAddressFast` is declared to return `{ min,max } | null`, yet
`null` is never returned.  Align the signature with behaviour (and the
wrapper above):

```diff
-): { min: Vector3; max: Vector3 } | null {
+): { min: Vector3; max: Vector3 } {
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  static fromBucketAddressFast(
    [x, y, z, _zoomStep]: Vector4,
    mag: Vector3,
  ): { min: Vector3; max: Vector3 } {
    /*
     The fast variant does not allocate a Bounding Box instance which can be helpful for tight loops.
     */
    const bucketSize = constants.BUCKET_WIDTH;

    // Precompute scaled sizes once
    const sx = bucketSize * mag[0];
    const sy = bucketSize * mag[1];
    const sz = bucketSize * mag[2];

    // Bucket bounds in world space
    const bxMin = x * sx;
    const byMin = y * sy;
    const bzMin = z * sz;
    const bxMax = bxMin + sx;
    const byMax = byMin + sy;
    const bzMax = bzMin + sz;

    return { min: [bxMin, byMin, bzMin], max: [bxMax, byMax, bzMax] };
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T14:53:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2156,32 +2168,112 @@ class DataApi {
   }
 
   /**
-   * Label voxels with the supplied value. Note that this method does not mutate
-   * the data immediately, but instead returns a promise (since the data might
-   * have to be downloaded first).
+   * Label voxels with the supplied value. This method behaves as if the user
+   * had brushed the provided voxels all at once. If the volume data wasn't
+   * downloaded completely yet, WEBKNOSSOS will merge the data as soon as it
+   * was downloaded.
    *
    * _Volume tracing only!_
    *
-   * @example // Set the segmentation id for some voxels to 1337
-   * await api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
+   * @example
+   * // Set the segmentation id for some voxels to 1337
+   * api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
    */
-  async labelVoxels(
-    voxels: Array<Vector3>,
+  labelVoxels(
+    globalPositionsMag1: Vector3[],
     label: number,
-    additionalCoordinates: AdditionalCoordinate[] | null = null,
-  ): Promise<void> {
-    assertVolume(Store.getState());
-    const segmentationLayer = this.model.getEnforcedSegmentationTracingLayer();
-    await Promise.all(
-      voxels.map((voxel) =>
-        segmentationLayer.cube._labelVoxelInAllResolutions_DEPRECATED(
-          voxel,
+    optAdditionalCoordinates?: AdditionalCoordinate[] | null,
+  ) {
+    const state = Store.getState();
+    const allowUpdate = state.annotation.restrictions.allowUpdate;
+    const additionalCoordinates = optAdditionalCoordinates ?? state.flycam.additionalCoordinates;
+    if (!allowUpdate || globalPositionsMag1.length === 0) return;
+
+    const volumeTracing = enforceActiveVolumeTracing(state);
+    const segmentationLayer = Model.getSegmentationTracingLayer(volumeTracing.tracingId);
+    const { cube } = segmentationLayer;
+    const magInfo = getMagInfo(segmentationLayer.mags);
+    const labeledZoomStep = magInfo.getClosestExistingIndex(0);
+    const labeledMag = magInfo.getMagByIndexOrThrow(labeledZoomStep);
+    const dimensionIndices = Dimensions.getIndices(OrthoViews.PLANE_XY);
+    const thirdDim = dimensionIndices[2];
+
+    const globalPositions = globalPositionsMag1.map(
+      (pos): Vector3 => [
+        Math.floor(pos[0] / labeledMag[0]),
+        Math.floor(pos[1] / labeledMag[1]),
+        Math.floor(pos[2] / labeledMag[2]),
+      ],
+    );
+    const groupedByW = _.groupBy(globalPositions, (pos) => pos[thirdDim]);
+
+    for (const [wValueStr, group] of Object.entries(groupedByW)) {
+      const w = Number.parseInt(wValueStr, 10);
+      const currentLabeledVoxelMap: LabeledVoxelsMap = new Map();
+
+      for (const pos of group) {
+        const bucketZoomedAddress = zoomedPositionToZoomedAddress(
+          pos,
+          labeledZoomStep,
           additionalCoordinates,
-          label,
-        ),
+        );
+
+        let labelMap = currentLabeledVoxelMap.get(bucketZoomedAddress);
+        if (!labelMap) {
+          labelMap = new Uint8Array(Constants.BUCKET_WIDTH ** 2);
+          currentLabeledVoxelMap.set(bucketZoomedAddress, labelMap);
+        }
+
+        const a = pos[dimensionIndices[0]];
+        const b = pos[dimensionIndices[1]];
+        const localA = mod(a, Constants.BUCKET_WIDTH);
+        const localB = mod(b, Constants.BUCKET_WIDTH);
+        labelMap[localA * Constants.BUCKET_WIDTH + localB] = 1;
+      }
+
+      const numberOfSlices = 1;
+      applyVoxelMap(
+        currentLabeledVoxelMap,
+        cube,
+        label,
+        (x, y, out) => {
+          out[0] = x;
+          out[1] = y;
+          out[2] = w;
+        },
+        numberOfSlices,
+        thirdDim,
+        true,
+        0,
path: frontend/javascripts/oxalis/api/api_latest.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Inefficient per-slice `applyVoxelMap` calls**

The loop runs `applyVoxelMap` and the expensive
`applyLabeledVoxelMapToAllMissingMags` once per Z slice.  For large
brushes affecting many slices this scales poorly (O(slices * buckets)).
Building one `LabeledVoxelsMap` for the entire call and invoking the
functions only once avoids repeated traversal and queueing.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T14:53:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2156,32 +2168,112 @@ class DataApi {
   }
 
   /**
-   * Label voxels with the supplied value. Note that this method does not mutate
-   * the data immediately, but instead returns a promise (since the data might
-   * have to be downloaded first).
+   * Label voxels with the supplied value. This method behaves as if the user
+   * had brushed the provided voxels all at once. If the volume data wasn't
+   * downloaded completely yet, WEBKNOSSOS will merge the data as soon as it
+   * was downloaded.
    *
    * _Volume tracing only!_
    *
-   * @example // Set the segmentation id for some voxels to 1337
-   * await api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
+   * @example
+   * // Set the segmentation id for some voxels to 1337
+   * api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
    */
-  async labelVoxels(
-    voxels: Array<Vector3>,
+  labelVoxels(
+    globalPositionsMag1: Vector3[],
     label: number,
-    additionalCoordinates: AdditionalCoordinate[] | null = null,
-  ): Promise<void> {
-    assertVolume(Store.getState());
-    const segmentationLayer = this.model.getEnforcedSegmentationTracingLayer();
-    await Promise.all(
-      voxels.map((voxel) =>
-        segmentationLayer.cube._labelVoxelInAllResolutions_DEPRECATED(
-          voxel,
+    optAdditionalCoordinates?: AdditionalCoordinate[] | null,
+  ) {
+    const state = Store.getState();
+    const allowUpdate = state.annotation.restrictions.allowUpdate;
+    const additionalCoordinates = optAdditionalCoordinates ?? state.flycam.additionalCoordinates;
+    if (!allowUpdate || globalPositionsMag1.length === 0) return;
+
+    const volumeTracing = enforceActiveVolumeTracing(state);
+    const segmentationLayer = Model.getSegmentationTracingLayer(volumeTracing.tracingId);
path: frontend/javascripts/oxalis/api/api_latest.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Explicit `null` additionalCoordinates are ignored**

```ts
const additionalCoordinates = optAdditionalCoordinates ?? state.flycam.additionalCoordinates;
```

Passing `null` intentionally is indistinguishable from passing
`undefined`; both fall back to the fly-cam coordinates.  This blocks a
caller from explicitly requesting “no additional coordinates”.

```diff
-const additionalCoordinates = optAdditionalCoordinates ?? state.flycam.additionalCoordinates;
+const additionalCoordinates =
+  optAdditionalCoordinates === undefined
+    ? state.flycam.additionalCoordinates
+    : optAdditionalCoordinates;
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T14:53:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,10 +18,11 @@ import type {
   Vector3,
 } from "oxalis/constants";
 import constants, { MappingStatusEnum } from "oxalis/constants";
+import Constants from "oxalis/constants";
 import { getMappingInfo } from "oxalis/model/accessors/dataset_accessor";
path: frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Duplicate default import from the same module**

`constants` is already imported as the default export on line 20. Importing the default again under the alias `Constants` on line 21 works syntactically, but:
• it adds 6 bytes to every bundle that includes this file, because the bundler deduplicates the module *after* symbol mangling,  
• it can easily confuse future readers (“are `constants` and `Constants` different?”).

Prefer a single import and reuse the same identifier, or re-export a named constant from `oxalis/constants` if you need a semantic distinction.

```diff
-import constants, { MappingStatusEnum } from "oxalis/constants";
-import Constants from "oxalis/constants";
+import constants, { MappingStatusEnum } from "oxalis/constants";
/* if you need an alias, do:
+const Constants = constants;
+*/
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import constants, { MappingStatusEnum } from "oxalis/constants";
/* if you need an alias, do:
const Constants = constants;
*/
import { getMappingInfo } from "oxalis/model/accessors/dataset_accessor";
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T14:53:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,7 +362,7 @@ export function applyVoxelMap(
           voxelMap,
           segmentId,
           get3DAddress,
-          sliceCount,
+          sliceOffset,
           thirdDimensionIndex,
path: frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts
user: login: philippotto
body: the param was simply renamed. the other call sites will still work.
created_at: 2025-05-07T06:17:07Z
author_association: MEMBER
diff_hunk: @@ -361,7 +362,7 @@ export function applyVoxelMap(
           voxelMap,
           segmentId,
           get3DAddress,
-          sliceCount,
+          sliceOffset,
           thirdDimensionIndex,
path: frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts
user: login: coderabbitai[bot]
body: @philippotto You're absolutely right. Since JavaScript/TypeScript function calls are position-based rather than name-based, simply renaming the parameter in the function definition won't break existing call sites. The variable rename from `sliceCount` to `sliceOffset` is a good improvement for code clarity as it better describes what the parameter actually represents - an offset value rather than a count.



(\_\_/)
(•ᴗ•)
/ >📝 Taking note of that!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: philippotto
PR: scalableminds/webknossos#8602
File: frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts:365-366
Timestamp: 2025-05-07T06:17:32.789Z
Learning: The parameter in applyVoxelMap was renamed from `sliceCount` to `sliceOffset` to better reflect its purpose, but this doesn't affect existing call sites since JavaScript/TypeScript function calls are position-based.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@philippotto You're absolutely right. Since JavaScript/TypeScript function calls are position-based rather than name-based, simply renaming the parameter in the function definition won't break existing call sites. The variable rename from `sliceCount` to `sliceOffset` is a good improvement for code clarity as it better describes what the parameter actually represents - an offset value rather than a count.



(\_\_/)
(•ᴗ•)
/ >📝 Taking note of that!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: philippotto
PR: scalableminds/webknossos#8602
File: frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts:365-366
Timestamp: 2025-05-07T06:17:32.789Z
Learning: The parameter in applyVoxelMap was renamed from `sliceCount` to `sliceOffset` to better reflect its purpose, but this doesn't affect existing call sites since JavaScript/TypeScript function calls are position-based.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-07T06:17:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2156,32 +2168,115 @@ class DataApi {
   }
 
   /**
-   * Label voxels with the supplied value. Note that this method does not mutate
-   * the data immediately, but instead returns a promise (since the data might
-   * have to be downloaded first).
+   * Label voxels with the supplied value. This method behaves as if the user
+   * had brushed the provided voxels all at once. If the volume data wasn't
+   * downloaded completely yet, WEBKNOSSOS will merge the data as soon as it
+   * was downloaded.
    *
    * _Volume tracing only!_
    *
-   * @example // Set the segmentation id for some voxels to 1337
-   * await api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
+   * @example
+   * // Set the segmentation id for some voxels to 1337
+   * api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
    */
-  async labelVoxels(
-    voxels: Array<Vector3>,
+  labelVoxels(
+    globalPositionsMag1: Vector3[],
     label: number,
-    additionalCoordinates: AdditionalCoordinate[] | null = null,
-  ): Promise<void> {
-    assertVolume(Store.getState());
-    const segmentationLayer = this.model.getEnforcedSegmentationTracingLayer();
-    await Promise.all(
-      voxels.map((voxel) =>
-        segmentationLayer.cube._labelVoxelInAllResolutions_DEPRECATED(
-          voxel,
+    optAdditionalCoordinates?: AdditionalCoordinate[] | null,
+  ) {
+    const state = Store.getState();
+    const allowUpdate = state.annotation.restrictions.allowUpdate;
+    const additionalCoordinates =
+      optAdditionalCoordinates === undefined
+        ? state.flycam.additionalCoordinates
+        : optAdditionalCoordinates;
+    if (!allowUpdate || globalPositionsMag1.length === 0) return;
+
+    const volumeTracing = enforceActiveVolumeTracing(state);
+    const segmentationLayer = Model.getSegmentationTracingLayer(volumeTracing.tracingId);
+    const { cube } = segmentationLayer;
+    const magInfo = getMagInfo(segmentationLayer.mags);
+    const labeledZoomStep = magInfo.getClosestExistingIndex(0);
+    const labeledMag = magInfo.getMagByIndexOrThrow(labeledZoomStep);
+    const dimensionIndices = Dimensions.getIndices(OrthoViews.PLANE_XY);
+    const thirdDim = dimensionIndices[2];
+
+    const globalPositions = globalPositionsMag1.map(
+      (pos): Vector3 => [
+        Math.floor(pos[0] / labeledMag[0]),
+        Math.floor(pos[1] / labeledMag[1]),
+        Math.floor(pos[2] / labeledMag[2]),
+      ],
+    );
+    const groupedByW = _.groupBy(globalPositions, (pos) => pos[thirdDim]);
+
+    for (const [wValueStr, group] of Object.entries(groupedByW)) {
+      const w = Number.parseInt(wValueStr, 10);
path: frontend/javascripts/oxalis/api/api_latest.ts
user: login: MichaelBuessemeyer
body: Not a must but:
why parse `w` to int again, when something like `group[0][thirdDim]` should also equal `w`? Thus, saving the conversion :thinking:?
created_at: 2025-05-07T09:22:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2156,32 +2168,115 @@ class DataApi {
   }
 
   /**
-   * Label voxels with the supplied value. Note that this method does not mutate
-   * the data immediately, but instead returns a promise (since the data might
-   * have to be downloaded first).
+   * Label voxels with the supplied value. This method behaves as if the user
+   * had brushed the provided voxels all at once. If the volume data wasn't
+   * downloaded completely yet, WEBKNOSSOS will merge the data as soon as it
+   * was downloaded.
    *
    * _Volume tracing only!_
    *
-   * @example // Set the segmentation id for some voxels to 1337
-   * await api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
+   * @example
+   * // Set the segmentation id for some voxels to 1337
+   * api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
    */
-  async labelVoxels(
-    voxels: Array<Vector3>,
+  labelVoxels(
+    globalPositionsMag1: Vector3[],
     label: number,
path: frontend/javascripts/oxalis/api/api_latest.ts
user: login: MichaelBuessemeyer
body: maybe rename this to `segmentId` as afaik this is what we usually call it?
created_at: 2025-05-07T09:32:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,7 +15,10 @@ import type { BoundingBoxObject, NumberLike } from "oxalis/store";
 import type { APIDataset, APIUser } from "types/api_types";
 import type { ArbitraryObject, Comparator } from "types/globals";
 
+export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
path: frontend/javascripts/libs/utils.ts
user: login: MichaelBuessemeyer
body: I'd put this into `globals.d.ts`
created_at: 2025-05-07T11:23:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,6 +128,7 @@ export class DataBucket {
     elementClass: ElementClass,
     zoomedAddress: BucketAddress,
     temporalBucketManager: TemporalBucketManager,
+    public containment: SomeContainment,
path: frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts
user: login: MichaelBuessemeyer
body: why did you mark this as public? public should be default in ts? This is the only location in the entire codebase where this is used  (expect for the `wk_dev` module)
created_at: 2025-05-07T11:32:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,13 +527,42 @@ export class DataBucket {
 
     const segmentId = castForArrayType(uncastSegmentId, data);
 
-    for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
-      for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
+    const limits = {
+      u: { min: 0, max: Constants.BUCKET_WIDTH },
+      v: { min: 0, max: Constants.BUCKET_WIDTH },
+      w: { min: 0, max: Constants.BUCKET_WIDTH },
+    };
+
+    if (this.containment.type === "partial") {
+      const plane = Dimensions.planeForThirdDimension(thirdDimensionIndex);
+      const [u, v, w] = Dimensions.getIndices(plane);
+      limits.u.min = this.containment.min[u];
+      limits.u.max = this.containment.max[u];
+
+      limits.v.min = this.containment.min[v];
+      limits.v.max = this.containment.max[v];
+
+      limits.w.min = this.containment.min[w];
+      limits.w.max = this.containment.max[w];
+    }
+
+    for (let firstDim = limits.u.min; firstDim < limits.u.max; firstDim++) {
+      for (let secondDim = limits.v.min; secondDim < limits.v.max; secondDim++) {
         if (voxelMap[firstDim * Constants.BUCKET_WIDTH + secondDim] === 1) {
           get3DAddress(firstDim, secondDim, out);
           const voxelToLabel = out;
path: frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts
user: login: MichaelBuessemeyer
body: This is not introduced by this pr, but why is the `Float32Array(3)` array created with the name `out`, then passed to a function and after that always renamed? Can't it simply be called `voxelToLabel` everywhere or do you see a specific reason for this?
created_at: 2025-05-07T11:44:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -272,40 +332,48 @@ class DataCube {
   // NULL_BUCKET if the bucket cannot possibly exist, e.g. because it is
   // outside the dataset's bounding box.
   getOrCreateBucket(address: BucketAddress): Bucket {
-    if (!this.isWithinBounds(address)) {
-      return this.getNullBucket();
-    }
-
-    let bucket = this.getBucket(address, true);
+    let bucket = this.getBucket(address);
 
     if (bucket instanceof NullBucket) {
-      bucket = this.createBucket(address);
+      const containment = this.checkContainment(address);
+      if (containment.type === "no") {
+        return this.getNullBucket();
+      }
+      bucket = this.createBucket(address, containment);
     }
 
     return bucket;
   }
 
   // Returns the Bucket object if it exists, or NULL_BUCKET otherwise.
-  getBucket(address: BucketAddress, skipBoundsCheck: boolean = false): Bucket {
-    if (!skipBoundsCheck && !this.isWithinBounds(address)) {
-      return this.getNullBucket();
-    }
-
+  getBucket(address: BucketAddress): Bucket {
     const [bucketIndex, cube] = this.getBucketIndexAndCube(address);
 
     if (bucketIndex != null && cube != null) {
       const bucket = cube.data.get(bucketIndex);
 
-      if (bucket != null) {
+      // We double-check that the address of the bucket matches the requested
+      // address. If the address is outside of the layer's bbox, the linearization
+      // of the address into one index might collide with another bucket (that is
+      // within the bbox) which is why the check is necessary.
+      // We use slice to ignore the additional coordinates (this is mostly done
+      // to ignore annoying cases like null vs [] which have identical semantics).
+      if (bucket != null && _.isEqual(address.slice(0, 4), bucket.zoomedAddress.slice(0, 4))) {
path: frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts
user: login: MichaelBuessemeyer
body: As you previously cared for preformance, `V4.isEqual` might be faster:

```suggestion
      if (bucket != null && V4.isEquals(address.slice(0, 4), bucket.zoomedAddress.slice(0, 4))) {
```
Note: Maybe casting is needed here for TS to assume the correct type and not complain.
created_at: 2025-05-07T12:04:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -269,3 +269,46 @@ describe("DataCube", () => {
     expect(index).toBe(10570);
   });
 });
+
+// This is not executed in the tests, but can be activated when needed
+// to make performance measurements for getOrCreateBucket
path: frontend/javascripts/test/model/binary/cube.spec.ts
user: login: MichaelBuessemeyer
body: Maybe add results from your testing to the comment :thinking:? This would help to see whether there were any performance changes. Else one has to checkout other code versions first to be able to check for performance changes
created_at: 2025-05-07T12:10:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +31,7 @@ class PullQueue {
   private abortController: AbortController;
   private consecutiveErrorCount: number;
   private isRetryScheduled: boolean;
+  private isDestroyed: boolean = false;
path: frontend/javascripts/oxalis/model/bucket_data_handling/pullqueue.ts
user: login: MichaelBuessemeyer
body: why is this needed now for the tests to work? While checking the changes in the test file I did not notice anything obvious why this might be needed now :thinking:
created_at: 2025-05-07T12:20:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +31,7 @@ class PullQueue {
   private abortController: AbortController;
   private consecutiveErrorCount: number;
   private isRetryScheduled: boolean;
+  private isDestroyed: boolean = false;
path: frontend/javascripts/oxalis/model/bucket_data_handling/pullqueue.ts
user: login: MichaelBuessemeyer
body: I just found something in `apiHelpers`. But this doesn't explain to me why this is now needed for testing and wasn't required previously
created_at: 2025-05-07T12:24:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2156,32 +2168,115 @@ class DataApi {
   }
 
   /**
-   * Label voxels with the supplied value. Note that this method does not mutate
-   * the data immediately, but instead returns a promise (since the data might
-   * have to be downloaded first).
+   * Label voxels with the supplied value. This method behaves as if the user
+   * had brushed the provided voxels all at once. If the volume data wasn't
+   * downloaded completely yet, WEBKNOSSOS will merge the data as soon as it
+   * was downloaded.
    *
    * _Volume tracing only!_
    *
-   * @example // Set the segmentation id for some voxels to 1337
-   * await api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
+   * @example
+   * // Set the segmentation id for some voxels to 1337
+   * api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
    */
-  async labelVoxels(
-    voxels: Array<Vector3>,
+  labelVoxels(
+    globalPositionsMag1: Vector3[],
     label: number,
-    additionalCoordinates: AdditionalCoordinate[] | null = null,
-  ): Promise<void> {
-    assertVolume(Store.getState());
-    const segmentationLayer = this.model.getEnforcedSegmentationTracingLayer();
-    await Promise.all(
-      voxels.map((voxel) =>
-        segmentationLayer.cube._labelVoxelInAllResolutions_DEPRECATED(
-          voxel,
+    optAdditionalCoordinates?: AdditionalCoordinate[] | null,
+  ) {
+    const state = Store.getState();
+    const allowUpdate = state.annotation.restrictions.allowUpdate;
+    const additionalCoordinates =
+      optAdditionalCoordinates === undefined
+        ? state.flycam.additionalCoordinates
+        : optAdditionalCoordinates;
+    if (!allowUpdate || globalPositionsMag1.length === 0) return;
+
+    const volumeTracing = enforceActiveVolumeTracing(state);
+    const segmentationLayer = Model.getSegmentationTracingLayer(volumeTracing.tracingId);
+    const { cube } = segmentationLayer;
+    const magInfo = getMagInfo(segmentationLayer.mags);
+    const labeledZoomStep = magInfo.getClosestExistingIndex(0);
+    const labeledMag = magInfo.getMagByIndexOrThrow(labeledZoomStep);
+    const dimensionIndices = Dimensions.getIndices(OrthoViews.PLANE_XY);
+    const thirdDim = dimensionIndices[2];
+
+    const globalPositions = globalPositionsMag1.map(
+      (pos): Vector3 => [
+        Math.floor(pos[0] / labeledMag[0]),
+        Math.floor(pos[1] / labeledMag[1]),
+        Math.floor(pos[2] / labeledMag[2]),
+      ],
+    );
+    const groupedByW = _.groupBy(globalPositions, (pos) => pos[thirdDim]);
+
+    for (const [wValueStr, group] of Object.entries(groupedByW)) {
+      const w = Number.parseInt(wValueStr, 10);
path: frontend/javascripts/oxalis/api/api_latest.ts
user: login: philippotto
body: good point!
created_at: 2025-05-08T12:53:48Z
author_association: MEMBER
diff_hunk: @@ -15,7 +15,10 @@ import type { BoundingBoxObject, NumberLike } from "oxalis/store";
 import type { APIDataset, APIUser } from "types/api_types";
 import type { ArbitraryObject, Comparator } from "types/globals";
 
+export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
path: frontend/javascripts/libs/utils.ts
user: login: philippotto
body: ah, I thought we had something like that but couldn't find it. thanks :)
created_at: 2025-05-08T12:56:18Z
author_association: MEMBER
diff_hunk: @@ -13,14 +13,15 @@
 import Toast from "libs/toast";
 import UserLocalStorage from "libs/user_local_storage";
 import * as Utils from "libs/utils";
-import { coalesce } from "libs/utils";
+import { type Writeable, coalesce, mod } from "libs/utils";
path: frontend/javascripts/oxalis/api/api_latest.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Added Writeable type import for typed mutability**

The `Writeable` type is imported to support mutable operations on readonly types like `BucketAddress`. However, there's a static analysis error indicating this type doesn't exist in the utils module.

```diff
-import { type Writeable, coalesce, mod } from "libs/utils";
+import { coalesce, mod } from "libs/utils";
+// Define the Writeable type locally or import from the correct location
+type Writeable<T> = { -readonly [P in keyof T]: T[P] };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { coalesce, mod } from "libs/utils";
// Define the Writeable type locally or import from the correct location
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: frontend-tests</summary>

[failure] 16-16: 
Module '"libs/utils"' has no exported member 'Writeable'.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T13:00:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,6 +128,7 @@ export class DataBucket {
     elementClass: ElementClass,
     zoomedAddress: BucketAddress,
     temporalBucketManager: TemporalBucketManager,
+    public containment: SomeContainment,
path: frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts
user: login: philippotto
body: this is a shorthand so that this property is automatically defined (and set upon instantiation) as a property. https://scm.slack.com/archives/CM6KG6R1V/p1726300490820709
I've started to integrated this at some places, as I find it quite concise and useful.
created_at: 2025-05-08T13:00:27Z
author_association: MEMBER
diff_hunk: @@ -509,13 +527,42 @@ export class DataBucket {
 
     const segmentId = castForArrayType(uncastSegmentId, data);
 
-    for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
-      for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
+    const limits = {
+      u: { min: 0, max: Constants.BUCKET_WIDTH },
+      v: { min: 0, max: Constants.BUCKET_WIDTH },
+      w: { min: 0, max: Constants.BUCKET_WIDTH },
+    };
+
+    if (this.containment.type === "partial") {
+      const plane = Dimensions.planeForThirdDimension(thirdDimensionIndex);
+      const [u, v, w] = Dimensions.getIndices(plane);
+      limits.u.min = this.containment.min[u];
+      limits.u.max = this.containment.max[u];
+
+      limits.v.min = this.containment.min[v];
+      limits.v.max = this.containment.max[v];
+
+      limits.w.min = this.containment.min[w];
+      limits.w.max = this.containment.max[w];
+    }
+
+    for (let firstDim = limits.u.min; firstDim < limits.u.max; firstDim++) {
+      for (let secondDim = limits.v.min; secondDim < limits.v.max; secondDim++) {
         if (voxelMap[firstDim * Constants.BUCKET_WIDTH + secondDim] === 1) {
           get3DAddress(firstDim, secondDim, out);
           const voxelToLabel = out;
path: frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts
user: login: philippotto
body: I think the original goal was to make it clearer that a variable is passed that is written to. 
created_at: 2025-05-08T13:01:39Z
author_association: MEMBER
diff_hunk: @@ -269,3 +269,46 @@ describe("DataCube", () => {
     expect(index).toBe(10570);
   });
 });
+
+// This is not executed in the tests, but can be activated when needed
+// to make performance measurements for getOrCreateBucket
path: frontend/javascripts/test/model/binary/cube.spec.ts
user: login: philippotto
body: I think, there's not much value in committing measurements here, as the numbers will be highly system dependent. When one is interested whether the performance got better or worse, one should execute the benchmark with the old code on the same machine in my opinion.
created_at: 2025-05-08T13:03:30Z
author_association: MEMBER
diff_hunk: @@ -31,6 +31,7 @@ class PullQueue {
   private abortController: AbortController;
   private consecutiveErrorCount: number;
   private isRetryScheduled: boolean;
+  private isDestroyed: boolean = false;
path: frontend/javascripts/oxalis/model/bucket_data_handling/pullqueue.ts
user: login: philippotto
body: I don't remember the specifics, but in the end the problem was that there were uncaught rejections (because bucket requests were aborted and the buckets were notified about this, even though the test was already finished). the unit tests themselves passed, but vitest complained because of the unhandled rejections. destroying the pullqueue seemed like a clean solution. isolating the individual tests even more would also be cool, but I think this is out of scope for this issue.
created_at: 2025-05-08T13:07:47Z
author_association: MEMBER
diff_hunk: @@ -1995,7 +2006,7 @@
       bucketPositionToGlobalAddress(bucketAddress, new MagInfo(magnifications));
 
     const nextBucketInDim = (bucket: BucketAddress, dim: 0 | 1 | 2) => {
-      const copy = bucket.slice() as BucketAddress;
+      const copy = bucket.slice() as Writeable<BucketAddress>;
path: frontend/javascripts/oxalis/api/api_latest.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix for missing Writeable type**

The code uses `Writeable` type, but there's a static analysis error indicating it doesn't exist in the utils module.


```diff
-      const copy = bucket.slice() as Writeable<BucketAddress>;
+      // Use a simpler approach without requiring the Writeable type
+      const copy = [...bucket] as BucketAddress;
```

Alternatively, you could define the Writeable type locally:

```diff
// At the top of the file with other imports
+type Writeable<T> = { -readonly [P in keyof T]: T[P] };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      // Use a simpler approach without requiring the Writeable type
      const copy = [...bucket] as BucketAddress;
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: frontend-tests</summary>

[failure] 2009-2009: 
Cannot find name 'Writeable'.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T13:26:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,13 +527,42 @@ export class DataBucket {
 
     const segmentId = castForArrayType(uncastSegmentId, data);
 
-    for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
-      for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
+    const limits = {
+      u: { min: 0, max: Constants.BUCKET_WIDTH },
+      v: { min: 0, max: Constants.BUCKET_WIDTH },
+      w: { min: 0, max: Constants.BUCKET_WIDTH },
+    };
+
+    if (this.containment.type === "partial") {
+      const plane = Dimensions.planeForThirdDimension(thirdDimensionIndex);
+      const [u, v, w] = Dimensions.getIndices(plane);
+      limits.u.min = this.containment.min[u];
+      limits.u.max = this.containment.max[u];
+
+      limits.v.min = this.containment.min[v];
+      limits.v.max = this.containment.max[v];
+
+      limits.w.min = this.containment.min[w];
+      limits.w.max = this.containment.max[w];
+    }
+
+    for (let firstDim = limits.u.min; firstDim < limits.u.max; firstDim++) {
+      for (let secondDim = limits.v.min; secondDim < limits.v.max; secondDim++) {
         if (voxelMap[firstDim * Constants.BUCKET_WIDTH + secondDim] === 1) {
           get3DAddress(firstDim, secondDim, out);
           const voxelToLabel = out;
path: frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts
user: login: MichaelBuessemeyer
body: well ok :shrug:
created_at: 2025-05-13T14:10:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -509,13 +527,42 @@ export class DataBucket {
 
     const segmentId = castForArrayType(uncastSegmentId, data);
 
-    for (let firstDim = 0; firstDim < Constants.BUCKET_WIDTH; firstDim++) {
-      for (let secondDim = 0; secondDim < Constants.BUCKET_WIDTH; secondDim++) {
+    const limits = {
+      u: { min: 0, max: Constants.BUCKET_WIDTH },
+      v: { min: 0, max: Constants.BUCKET_WIDTH },
+      w: { min: 0, max: Constants.BUCKET_WIDTH },
+    };
+
+    if (this.containment.type === "partial") {
+      const plane = Dimensions.planeForThirdDimension(thirdDimensionIndex);
+      const [u, v, w] = Dimensions.getIndices(plane);
+      limits.u.min = this.containment.min[u];
+      limits.u.max = this.containment.max[u];
+
+      limits.v.min = this.containment.min[v];
+      limits.v.max = this.containment.max[v];
+
+      limits.w.min = this.containment.min[w];
+      limits.w.max = this.containment.max[w];
+    }
+
path: frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Clamp containment limits to bucket bounds to avoid out-of-range indices**

`limits.u|v|w.min/max` are taken directly from `this.containment.min/max`.  
When the user shrinks the bounding-box aggressively, these values can be
outside the legal bucket voxel range `0…Constants.BUCKET_WIDTH` (e.g. `-5`
or `33`).  
That produces negative or too-large indices for

```ts
voxelMap[firstDim * Constants.BUCKET_WIDTH + secondDim]
```

and for the subsequent modulo arithmetic, leading to undefined data or
hard-to-trace `RangeError`s.

Consider clamping once right after the assignment:

```diff
       limits.u.min = this.containment.min[u];
-      limits.u.max = this.containment.max[u];
+      limits.u.min = Math.max(0, this.containment.min[u]);
+      limits.u.max = Math.min(Constants.BUCKET_WIDTH, this.containment.max[u]);

       limits.v.min = this.containment.min[v];
-      limits.v.max = this.containment.max[v];
+      limits.v.min = Math.max(0, this.containment.min[v]);
+      limits.v.max = Math.min(Constants.BUCKET_WIDTH, this.containment.max[v]);

       limits.w.min = this.containment.min[w];
-      limits.w.max = this.containment.max[w];
+      limits.w.max = Math.min(Constants.BUCKET_WIDTH, this.containment.max[w]);

+      // Bail out early if nothing of this bucket is inside the bbox
+      if (
+        limits.u.min >= limits.u.max ||
+        limits.v.min >= limits.v.max ||
+        limits.w.min >= limits.w.max
+      ) {
+        return false;
+      }
```

This keeps all subsequent array accesses in-bounds and avoids work on
completely clipped buckets.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T11:34:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2156,32 +2169,115 @@ class DataApi {
   }
 
   /**
-   * Label voxels with the supplied value. Note that this method does not mutate
-   * the data immediately, but instead returns a promise (since the data might
-   * have to be downloaded first).
+   * Label voxels with the supplied value. This method behaves as if the user
+   * had brushed the provided voxels all at once. If the volume data wasn't
+   * downloaded completely yet, WEBKNOSSOS will merge the data as soon as it
+   * was downloaded.
    *
    * _Volume tracing only!_
    *
-   * @example // Set the segmentation id for some voxels to 1337
-   * await api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
+   * @example
+   * // Set the segmentation id for some voxels to 1337
+   * api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
    */
-  async labelVoxels(
-    voxels: Array<Vector3>,
-    label: number,
-    additionalCoordinates: AdditionalCoordinate[] | null = null,
-  ): Promise<void> {
-    assertVolume(Store.getState());
-    const segmentationLayer = this.model.getEnforcedSegmentationTracingLayer();
-    await Promise.all(
-      voxels.map((voxel) =>
-        segmentationLayer.cube._labelVoxelInAllResolutions_DEPRECATED(
-          voxel,
+  labelVoxels(
+    globalPositionsMag1: Vector3[],
+    segmentId: number,
+    optAdditionalCoordinates?: AdditionalCoordinate[] | null,
+  ) {
+    const state = Store.getState();
+    const allowUpdate = state.annotation.restrictions.allowUpdate;
+    const additionalCoordinates =
+      optAdditionalCoordinates === undefined
+        ? state.flycam.additionalCoordinates
+        : optAdditionalCoordinates;
+    if (!allowUpdate || globalPositionsMag1.length === 0) return;
+
path: frontend/javascripts/viewer/api/api_latest.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Guard against missing finest magnification (zoom step 0)**

`magInfo.getClosestExistingIndex(0)` is assumed to return a number, but the
implementation can return `null` if the dataset does not offer a mag at
zoom-step 0.  In that case the subsequent call
`magInfo.getMagByIndexOrThrow(labeledZoomStep)` will throw.

```ts
const labeledZoomStep = magInfo.getClosestExistingIndex(0);
if (labeledZoomStep == null) {
  throw new Error("No magnification level corresponding to zoom-step 0 found.");
}
```

Failing fast with a clear message prevents the user from seeing an
ambiguous runtime crash later on.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T11:34:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-06T14:15:03Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

This change introduces containment-aware logic to prevent labeling voxels outside the dataset's bounding box. Bucket creation and voxel labeling routines are updated to compute and enforce containment, restricting voxel writes to regions within the bounding box. Related tests and type definitions are updated, and a changelog entry documents the fix.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `CHANGELOG.unreleased.md` | Added entry describing fix for preventing brushing voxels outside layer bounding box. |
| `frontend/javascripts/libs/utils.ts`, `frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts`, `frontend/javascripts/viewer/model/reducers/reducer_helpers.ts` | Whitespace/comment changes only. |
| `frontend/javascripts/types/globals.d.ts` | Added comment header and new utility type `Writeable<T>`. |
| `frontend/javascripts/viewer/constants.ts` | Made `BucketAddress` and vector index arrays readonly tuples. |
| `frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts` | Refactored bounding box construction and intersection, removed/added methods for fast containment checks. |
| `frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts` | Added containment property and logic to restrict voxel writes to contained regions. Updated relevant method signatures. |
| `frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts` | Replaced bounds check with detailed containment logic, updated bucket creation and retrieval to use containment, removed deprecated labeling method. |
| `frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts` | Added `destroy()` method and destruction flag for controlled teardown. |
| `frontend/javascripts/viewer/api/api_latest.ts` | Rewrote `labelVoxels` to use bulk labeling with containment, updated imports and types. |
| `frontend/javascripts/viewer/api/wk_dev.ts` | Added public getter for `model` singleton. |
| `frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts` | Guard added to prevent updates for segment ID 0. |
| `frontend/javascripts/viewer/model_initialization.ts` | Updated bounding box assignment logic to use fallback layer bounding box if available. |
| `frontend/javascripts/test/helpers/apiHelpers.ts` | Replaced explicit cleanup with unified `destroy()` call on pull queue. |
| `frontend/javascripts/test/model/binary/cube.spec.ts`, `frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts`, `frontend/javascripts/test/model/binary/pullqueue.spec.ts`, `frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts`, `frontend/javascripts/test/model/texture_bucket_manager.spec.ts` | Updated `DataBucket` instantiations to include containment argument. Minor test improvements and benchmark addition. |
| `frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts` | Enhanced tests for voxel labeling, especially for bounding box and z-coordinate boundary conditions. |
| `docs/volume_annotation/segments_list.md` | Updated documentation to clarify segment visibility toggling and related UI settings. |

## Assessment against linked issues

| Objective | Addressed | Explanation |
|-----------|-----------|-------------|
| Prevent labeling data outside of the dataset's bounding box (#6303) | ✅ |  |
| Ensure no data is produced or stored outside the bounding box for labeling (#6303) | ✅ |  |
| Update relevant code paths to enforce bounding box containment during labeling (#6303) | ✅ |  |
| Add or update tests to verify no out-of-bounds labeling occurs (#6303) | ✅ |  |

## Suggested labels

`bug`, `backend`, `frontend`

## Suggested reviewers

- philippotto

## Poem

> A rabbit hops with bounding pride,  
> No voxels brushed outside the side!  
> Containment checked, the buckets neat,  
> No stray data beneath our feet.  
> Bounding boxes safe and sound,  
> Only label what's in bounds!  
> 🐇✨

</details>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=scalableminds/webknossos&utm_content=8602):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: philippotto
created_at: 2025-05-08T13:19:56Z
body: Thank you for your feedback! Could you have another look? :)

> Testing: Everything worked as expected except for one small detail: During testing with restricted mags (4-4-1) the area where drawing is allowed is a little off by -1 in x and y direction (did not test z direction). Please see the image: The gray DS border is not clearly where the turquoise border (my brushing) is. When no mag restriction was in place, everything worked fine :D ![image](https://private-user-images.githubusercontent.com/39529669/441245441-ed6b4891-6827-4dd8-a6de-116b368d77ad.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY3MDg4NzIsIm5iZiI6MTc0NjcwODU3MiwicGF0aCI6Ii8zOTUyOTY2OS80NDEyNDU0NDEtZWQ2YjQ4OTEtNjgyNy00ZGQ4LWE2ZGUtMTE2YjM2OGQ3N2FkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA4VDEyNDkzMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTVjZWZlN2E4OTIzYTFkZmMzMTc0ZDY2NjFjMTBhNzViZGFkYmJkYThhZDMxYmQzYjBjZWZkOWRhZmEzNzRkNDMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.544xaqZf-viZV1euGVdxo9f-mhMorf0cINtEG4EW1_Y)

Good find :+1: I had a look and this only happens if the bbox is not aligned with the mag restriction. In that case, it's not possible to cut off the labeling at the exact border. Either, we crop too much or too little. Have a look at this screenshot where you can see what the smallest "voxel unit" is and that the border goes through it.

![image](https://github.com/user-attachments/assets/1e449940-7684-4851-96b0-b6a7a62579b0)

Therefore, I think, that it is alright to let the user brush a bit over the bbox limit as otherwise, there would be an area within the bbox which cannot be labeled. I had to adapt the rounding code so that it also works similarily for annotating at the bottom-right border. See my latest commit :)
author_association: MEMBER
user: login: philippotto
created_at: 2025-05-14T12:13:07Z
body: > Once the CI is green and this branch is up to date with the master I'll give this another quick re-test and then it should be ready to go 🟢

it's green now :)
author_association: MEMBER
user: login: philippotto
created_at: 2025-05-16T09:50:51Z
body: Thanks for raising this! This is caused because the color layer was bigger than the segmentation layer. I tracked down the bug and found out that the bounding box of the segmentation tracing was always set to the union of all layer bboxes. However, this should only happen for empty volume tracings (without fallback). I pushed a fix. Please have another look :)
author_association: MEMBER