Skip to content

Don't label outside of layer's bbox #8602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
May 19, 2025
Merged

Conversation

philippotto
Copy link
Member

@philippotto philippotto commented May 6, 2025

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:


(Please delete unneeded items, merge only when none are left open)

@philippotto philippotto self-assigned this May 6, 2025
Copy link
Contributor

coderabbitai bot commented May 6, 2025

📝 Walkthrough

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

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

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.


Note

⚡️ Faster reviews with caching

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.

✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • 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 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 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 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 for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@philippotto philippotto changed the title [WIP] Don't label outside of layer's bbox Don't label outside of layer's bbox May 6, 2025
@philippotto philippotto marked this pull request as ready for review May 6, 2025 14:45
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (1)
frontend/javascripts/oxalis/shaders/coords.glsl.ts (1)

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:

- // todop: remove again
- return false;
🧹 Nitpick comments (12)
CHANGELOG.unreleased.md (1)

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".

-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)
🧰 Tools
🪛 LanguageTool

[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)

frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts (1)

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:

 if (segmentId === 0) {
+  // Skip updates for segment ID 0 as it represents background/empty space
   return segments;
 }
frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts (1)

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.

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

frontend/javascripts/oxalis/constants.ts (1)

52-56: Spelling: Vector*IndiciesVector*Indices

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

-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.)

frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (2)

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.

frontend/javascripts/test/model/volumetracing/volume_annotation_sampling.spec.ts (2)

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.

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

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:

- 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;
frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts (1)

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.

frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)

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:

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

-    // 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.",
+      );
+    }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 228f681 and aa12cc5.

📒 Files selected for processing (20)
  • 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)
🧰 Additional context used
🧬 Code Graph Analysis (9)
frontend/javascripts/test/model/binary/pullqueue.spec.ts (1)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)
  • DataBucket (93-833)
frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts (1)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)
  • DataBucket (93-833)
frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts (2)
frontend/javascripts/libs/utils.ts (1)
  • Writeable (18-18)
frontend/javascripts/oxalis/constants.ts (1)
  • BucketAddress (22-24)
frontend/javascripts/test/model/binary/cube.spec.ts (2)
frontend/javascripts/oxalis/constants.ts (1)
  • Vector3 (13-13)
frontend/javascripts/oxalis/model/helpers/mag_info.ts (1)
  • MagInfo (11-225)
frontend/javascripts/oxalis/api/wk_dev.ts (1)
frontend/javascripts/oxalis/singletons.ts (1)
  • Model (7-7)
frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts (2)
frontend/javascripts/oxalis/constants.ts (3)
  • Vector3 (13-13)
  • BoundingBoxType (41-44)
  • Vector4 (14-14)
frontend/javascripts/libs/mjs.ts (1)
  • V3 (398-398)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)
frontend/javascripts/oxalis/constants.ts (1)
  • Vector3 (13-13)
frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (3)
  • DataBucket (93-833)
  • Containment (80-91)
  • Bucket (70-70)
frontend/javascripts/oxalis/constants.ts (2)
  • Vector3 (13-13)
  • BucketAddress (22-24)
frontend/javascripts/libs/utils.ts (1)
  • mod (24-26)
frontend/javascripts/oxalis/constants.ts (1)
webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala (2)
  • AdditionalCoordinate (90-95)
  • AdditionalCoordinate (97-112)
🪛 LanguageTool
CHANGELOG.unreleased.md

[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)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
🔇 Additional comments (19)
frontend/javascripts/libs/utils.ts (1)

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.

frontend/javascripts/oxalis/model/bucket_data_handling/pullqueue.ts (3)

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.

frontend/javascripts/test/model/texture_bucket_manager.spec.ts (1)

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.

frontend/javascripts/test/helpers/apiHelpers.ts (1)

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.

frontend/javascripts/oxalis/api/wk_dev.ts (2)

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.

frontend/javascripts/test/model/binary/pullqueue.spec.ts (1)

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.

frontend/javascripts/test/model/binary/temporal_bucket_manager.spec.ts (1)

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

frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.ts (1)

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

frontend/javascripts/test/model/binary/cube.spec.ts (1)

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.

frontend/javascripts/oxalis/constants.ts (1)

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!

frontend/javascripts/oxalis/api/api_latest.ts (2)

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:

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

frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)

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

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.

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

frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts (1)

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)

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:

+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.

frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (1)

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:

-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;
+*/
🧹 Nitpick comments (3)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)

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:

+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.

frontend/javascripts/oxalis/api/api_latest.ts (2)

2190-2192: Explicit null additionalCoordinates are ignored

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:

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

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between aa12cc5 and fd87641.

📒 Files selected for processing (4)
  • 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)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (1)
frontend/javascripts/oxalis/constants.ts (1)
  • Vector3 (13-13)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
🔇 Additional comments (8)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (3)

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.

frontend/javascripts/oxalis/api/api_latest.ts (1)

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.

frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (4)

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.

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for this PR and also improving performance 🎉 (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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (1)

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.

-import constants, { MappingStatusEnum } from "oxalis/constants";
-import Constants from "oxalis/constants";
+import constants, { MappingStatusEnum } from "oxalis/constants";
+const Constants = constants;
frontend/javascripts/oxalis/api/api_latest.ts (3)

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:

-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

const w = parseInt(thirdDim);

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

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

2190-2192: 🛠️ Refactor suggestion

Handling of optional additionalCoordinates could be improved

The current implementation:

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.

-const additionalCoordinates =
-  optAdditionalCoordinates === undefined
-    ? state.flycam.additionalCoordinates
-    : optAdditionalCoordinates;
+const additionalCoordinates =
+  optAdditionalCoordinates === undefined
+    ? state.flycam.additionalCoordinates
+    : optAdditionalCoordinates;
🧹 Nitpick comments (1)
frontend/javascripts/oxalis/api/api_latest.ts (1)

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.

-const numberOfSlices = 1;
+const sliceOffset = 1;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between fd87641 and a41777c.

📒 Files selected for processing (4)
  • 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)
✅ Files skipped from review due to trivial changes (2)
  • frontend/javascripts/libs/utils.ts
  • frontend/javascripts/types/globals.d.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (5)
  • DataBucket (95-835)
  • Containment (89-93)
  • NullBucket (49-67)
  • Bucket (70-70)
  • SomeContainment (80-88)
frontend/javascripts/oxalis/constants.ts (3)
  • Vector3 (13-13)
  • BucketAddress (22-24)
  • Vector4 (14-14)
frontend/javascripts/libs/utils.ts (1)
  • mod (22-24)
🪛 GitHub Check: frontend-tests
frontend/javascripts/oxalis/api/api_latest.ts

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

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
🔇 Additional comments (16)
frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (12)

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.

frontend/javascripts/oxalis/api/api_latest.ts (4)

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.

…rder so that one can label the full bbox (and a bit more consequentially)
@philippotto
Copy link
Member Author

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

Good find 👍 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

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 :)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (1)

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.

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:

-import constants, { MappingStatusEnum } from "oxalis/constants";
-import Constants from "oxalis/constants";
+import Constants, { MappingStatusEnum } from "oxalis/constants";
🧹 Nitpick comments (1)
frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (1)

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.

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between a41777c and 0496938.

📒 Files selected for processing (2)
  • frontend/javascripts/oxalis/api/api_latest.ts (12 hunks)
  • frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (7 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (3)
frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts (4)
  • DataBucket (95-835)
  • Containment (89-93)
  • Bucket (70-70)
  • SomeContainment (80-88)
frontend/javascripts/oxalis/constants.ts (3)
  • Vector3 (13-13)
  • BucketAddress (22-24)
  • Vector4 (14-14)
frontend/javascripts/libs/utils.ts (1)
  • mod (22-24)
🪛 GitHub Check: frontend-tests
frontend/javascripts/oxalis/api/api_latest.ts

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

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
🔇 Additional comments (18)
frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts (9)

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.

frontend/javascripts/oxalis/api/api_latest.ts (9)

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.

@MichaelBuessemeyer MichaelBuessemeyer self-requested a review May 12, 2025 16:28
Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 🟢

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts (1)

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:

+/**
+ * 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();
 }
CHANGELOG.unreleased.md (1)

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:

- 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)
🧰 Tools
🪛 LanguageTool

[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)

frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts (1)

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.

frontend/javascripts/viewer/api/api_latest.ts (2)

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.

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

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.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0496938 and f6a3ff4.

📒 Files selected for processing (19)
  • 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)
✅ Files skipped from review due to trivial changes (3)
  • 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
🚧 Files skipped from review as they are similar to previous changes (8)
  • 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
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/javascripts/viewer/api/api_latest.ts (4)
frontend/javascripts/types/globals.d.ts (1)
  • Writeable (25-25)
frontend/javascripts/types/api_types.ts (1)
  • AdditionalCoordinate (30-30)
frontend/javascripts/libs/utils.ts (1)
  • mod (22-24)
frontend/javascripts/viewer/model/volumetracing/volume_annotation_sampling.ts (1)
  • applyVoxelMap (304-377)
🪛 LanguageTool
CHANGELOG.unreleased.md

[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)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
🔇 Additional comments (17)
frontend/javascripts/viewer/model/bucket_data_handling/pullqueue.ts (2)

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.

frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts (1)

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.

frontend/javascripts/viewer/constants.ts (2)

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.

frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts (5)

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:

✅ Verification successful

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:

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

Length of output: 141


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.

frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts (7)

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.

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

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:

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

+if (!areBoundingBoxesOverlappingOrTouching(this.boundingBox, bucketBBox)) {
+  return NoContainment;
+}
  1. 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:

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 ]

Comment on lines +2184 to +2195
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;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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.

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.

@philippotto
Copy link
Member Author

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 :)

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hej I just gave this another test and it doesn't seem to work correctly anymore, or I am testing it wrong 🤔

Here is a screenshot of my test:
image
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.

…on tracings (in case of an existing fallback segmentation, that layer should be used)
@philippotto
Copy link
Member Author

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 :)

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

#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

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good and works now as expected 🎉 👯 . Thanks a lot for applying all the feedback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
CHANGELOG.unreleased.md (1)

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:

- Voxels outside of the layer bounding box cannot be brushed, anymore. [#8602]
+ Voxels outside the layer bounding box can no longer be brushed. [#8602]
🧰 Tools
🪛 LanguageTool

[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)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c356887 and d59c476.

📒 Files selected for processing (2)
  • CHANGELOG.unreleased.md (1 hunks)
  • frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts
🧰 Additional context used
🪛 LanguageTool
CHANGELOG.unreleased.md

[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)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: frontend-tests
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests

@philippotto philippotto enabled auto-merge (squash) May 19, 2025 12:56
@philippotto philippotto merged commit 7892599 into master May 19, 2025
5 checks passed
@philippotto philippotto deleted the no-labeling-outside-bbox branch May 19, 2025 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Don't label data outside of dataset bbox
2 participants