Skip to content
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

Allow permanent dataset layer rotation in dataset settings #8159

Open
wants to merge 29 commits into
base: master
Choose a base branch
from

Conversation

MichaelBuessemeyer
Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer commented Oct 30, 2024

--> This PR adds new settings to the dataset settings for each layer. An interface (still in design process) enables the user to set a affine transformation matrix by letting the user define angles for each axis around which the dataset should be rotated.

  • https://___.webknossos.xyz

Steps to test:

  • Basic testing:
    • View your favourite DS
    • open the DS settings in another tab. Use the rotation slider to configure some rotation. e.g. z 90 degrees and save
    • reload the view DS tab. The DS should now be fully rotated by 90 degree around the z axis -> xy viewport should be rotated by 90 degrees
    • Play around with toggeling the rotation off and on in the layer setting in the view mode
  • Testing annotation support
    • create a hybrid annotation of a rotated DS
    • add an empty volume layer
    • turn of the transforms
    • toggling the transforms should not work on the skeleton layer and the volume layer without fallback. These do not have an underlying layer that might have transforms configured. Therefore, nativelyRenderedLayerName cannot be set to one of these layers. Moreover, the transforms cannot be toggled on on such layers as they do not have transformations.
    • toggle the transforms on other layer and have fun
    • toggle off the transforms, do volume annotations on each layer and create some skeleton nodes
    • toggle on the transforms again. The nodes and volume annotation should rotate accordingly. This also applies to the volume layer without fallback data.
    • This should hopefully cover all wanted annotation scenarios. If you can think up more, please also test them 🙏
  • In general: When toggling transformations, the camera position should update accordingly.
  • Check conflicting nativelyRenderedLayerName settings.
    • Open a hybrid annotation on a dataset with a rotation setting that has at least one segmentation layer.
    • On the editable segmentation layer (volume layer with fallback) use the "transform button" to render the volume layer natively
    • In another tab open another such hybrid tracing. The DatasetViewConfigs should now have nativelyRenderedLayerName set to the tracingId of the volume layer of the first annotation. This does not exist in the newly opened other annotation. => The annotation should still open up (but with all layers transformed), as the nativelyRenderedLayerName setting should be automatically disregarded as the layer does not exist in this annotation.
  • !!! important !!!
    • Please also test whether a DS with multi modality still works as expected 🙏

TODOs / Problems:

  • [ ] Design a visualization of the result. For some first ideas look at the issue #7334 --> deferred / not planned
  • The rotation should be done around the center of the dataset. For this, a translation by the dataset center is needed
  • The interface works by defining Euler angles. The problem is that Euler angles are not deterministic / there are multiple ways to express a certain rotation with different Euler angle values. See: https://stackoverflow.com/questions/32235395/does-euler-angles-quaternion-euler-angles-always-result-in-an-equivalent-r
    • This makes it hard to update the settings correctly -> a 180 degree rotation around y is equivalent to 180 on x and 180 on z axis :/
    • A solution for this is needed as the interface currently is pretty quirky.
  • Reset nativelyRenderedLayerName in model init if this layer does not exist. (Might happen when switching annotations)

Problems:

  • Layer rotates out of the dataset bbox

Issues:


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

Copy link
Contributor

coderabbitai bot commented Oct 30, 2024

📝 Walkthrough

Walkthrough

This pull request introduces comprehensive changes to dataset layer transformations across multiple frontend files. The modifications focus on enhancing dataset rotation capabilities, reorganizing transformation-related functions, and improving the user interface for layer transformations. Key changes include creating a new dataset_layer_transformation_accessor.ts module, updating type definitions, and implementing rotation controls for dataset layers.

Changes

File Change Summary
frontend/javascripts/oxalis/model/accessors/dataset_layer_transformation_accessor.ts New module created with comprehensive transformation handling functions
frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx Added components for managing dataset rotation along x, y, z axes
frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx Integrated axis rotation settings component
Multiple files Relocated transformation-related imports from dataset_accessor to new transformation accessor module

Assessment against linked issues

Objective Addressed Explanation
Rotate dataset layers Implemented rotation controls and transformation mechanisms
Persist rotation Transformations can be configured and saved
Handle skeleton layer transformations Transformation logic supports skeleton layers

Possibly related PRs

Suggested labels

usability

Suggested reviewers

  • philippotto
  • daniel-wer

Poem

🐰 Rotation's dance, a matrix's grace,
Layers twirl in digital space
Transforms align with pixel's might
Bringing datasets to new height!
Code rabbits spin their magic wheel 🌀


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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

Michael Büßemeyer added 21 commits October 30, 2024 16:45
… according to affine matrix coordTransform
-  also always translate by dataset bounding box and not by layer bounding box for consistent rotation results
…a single one.

- in case layers have the same transformation, the automatic inverse of the natively rendered layer (applied to all other layers) will cancel out the layers transformation
- and fixing the code according to the logic of only saving one native layer
…t datasetconfig when nativelyRenderedLayerName is not present in current view / dataset
@MichaelBuessemeyer MichaelBuessemeyer marked this pull request as ready for review December 6, 2024 13:52
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: 4

🧹 Outside diff range and nitpick comments (19)
frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (1)

153-177: Avoid unnecessary re-renders by memoizing Tooltip content

The JSX returned inside the Tooltip component creates a new anonymous function on each render. To prevent unnecessary re-renders and optimize performance, consider memoizing the tooltip content or extracting it into a separate component.

frontend/javascripts/admin/dataset/composition_wizard/04_configure_new_dataset.tsx (3)

27-28: Remove unused import 'getReadableURLPart'

The getReadableURLPart function is imported but not used in the file. Removing unused imports can improve code readability and prevent potential maintenance issues.

Apply this diff to remove the unused import:

 import _ from "lodash";
 import messages from "messages";
-import { getReadableURLPart } from "oxalis/model/accessors/dataset_accessor";
 import { flatToNestedMatrix } from "oxalis/model/accessors/dataset_layer_rotation_accessor";

Line range hint 119-123: Use dependency array in useEffectOnlyOnce

The handleTransformImport function is defined outside the useEffectOnlyOnce, but it's not included in its dependency array. Although useEffectOnlyOnce implies it runs only once, adding dependencies ensures correct behavior if the hook's implementation changes.


Line range hint 174-178: Improve error message clarity

The error message in the exception "Cannot create dataset without being logged in." could be more user-friendly by rephrasing it.

Consider rephrasing the error message:

-throw new Error("Cannot create dataset without being logged in.");
+throw new Error("User must be logged in to create a dataset.");
frontend/javascripts/oxalis/model/accessors/dataset_layer_rotation_accessor.ts (1)

366-412: Simplify 'doAllLayersHaveTheSameRotation' function

The function can be simplified for better readability and maintainability. Consider refactoring nested conditions and loops.

frontend/javascripts/oxalis/model/accessors/dataset_accessor.ts (3)

Line range hint 55-58: Remove unused function 'doesSupportVolumeWithFallback'

The function doesSupportVolumeWithFallback is defined but not used anywhere in the codebase. Removing unused code improves maintainability.


Line range hint 264-269: Avoid directly modifying function arguments

In getDatasetExtentAsProduct, the function takes extent as an argument and might be modifying it if not careful. Ensure that the function does not have side effects.


Line range hint 379-384: Handle potential null values in 'getMappingInfoOrNull'

The function getMappingInfoOrNull accesses activeMappingInfos[layerName] without checking if layerName is in activeMappingInfos. This could lead to undefined behavior if layerName is not present.

Apply this diff to add a check:

 export function getMappingInfoOrNull(
   activeMappingInfos: Record<string, ActiveMappingInfo>,
   layerName: string | null | undefined,
 ): ActiveMappingInfo | null {
-  if (layerName != null && activeMappingInfos[layerName]) {
+  if (layerName != null && layerName in activeMappingInfos) {
     return activeMappingInfos[layerName];
   }
   return null;
 }
frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (2)

247-254: Clarify condition logic in 'TransformationIcon'

The complex condition in isDisabled could be difficult to read. Consider simplifying or adding comments to clarify the logic.


797-798: Replace 'LockOutlined' and 'UnlockOutlined' icons

In Ant Design v4, the LockOutlined and UnlockOutlined icons are deprecated. Consider replacing them with the updated icons from @ant-design/icons.

frontend/javascripts/types/globals.d.ts (1)

19-21: LGTM! Consider documenting type usage

The Mutable<T> type is well-defined using TypeScript's mapped type feature. This utility type will be helpful for working with dataset rotation settings where immutable types need to be temporarily mutable.

Consider adding a brief JSDoc comment explaining when to use this type, as removing readonly modifiers should be done judiciously. Example:

/**
 * Removes readonly modifiers from all properties of type T.
 * Use sparingly, primarily for initialization of immutable data structures.
 */
export type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};
frontend/javascripts/oxalis/geometries/materials/edge_shader.ts (2)

Line range hint 47-52: Add error handling for invalid layer names

The transformation matrix calculation assumes the layer exists but doesn't handle invalid nativelyRenderedLayerName values.

Add error handling:

 this.uniforms["transform"] = {
   value: M4x4.transpose(
-    getTransformsForSkeletonLayer(dataset, nativelyRenderedLayerName).affineMatrix,
+    (() => {
+      try {
+        return getTransformsForSkeletonLayer(dataset, nativelyRenderedLayerName).affineMatrix;
+      } catch (error) {
+        console.error("Failed to get transforms:", error);
+        return M4x4.identity();  // Fallback to identity matrix
+      }
+    })(),
   ),
 };

Line range hint 71-89: Consider debouncing transform updates

The store listener for transformation changes could trigger frequent material updates. Consider debouncing these updates for better performance.

 listenToStoreProperty(
   (storeState) =>
     getTransformsForSkeletonLayer(
       storeState.dataset,
       storeState.datasetConfiguration.nativelyRenderedLayerName,
     ),
-  (skeletonTransforms) => {
+  _.debounce((skeletonTransforms) => {
     const transforms = skeletonTransforms;
     const { affineMatrix } = transforms;
     // ... rest of the handler
-  },
+  }, 16),  // Debounce to ~60fps
   true,
 );
frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts (1)

28-33: Consider adding input validation for BoundingBoxObject dimensions.

While the implementation is correct, it might be good to add validation to ensure that width, height, and depth are non-negative values to prevent invalid bounding boxes.

 static fromBoundBoxObject(boundingBox: BoundingBoxObject): BoundingBox {
+  if (boundingBox.width < 0 || boundingBox.height < 0 || boundingBox.depth < 0) {
+    throw new Error('BoundingBoxObject dimensions must be non-negative');
+  }
   return new BoundingBox({
     min: boundingBox.topLeft,
     max: V3.add(boundingBox.topLeft, [boundingBox.width, boundingBox.height, boundingBox.depth]),
   });
 }
frontend/javascripts/libs/mjs.ts (1)

223-226: Consider adding type safety to the identity matrix.

The implementation is correct, but consider adding type checking to ensure the returned identity matrix matches the expected Matrix4x4 type.

 identity(): Matrix4x4 {
-  return BareM4x4.identity;
+  const identityMatrix = BareM4x4.identity;
+  if (!(identityMatrix instanceof Float32Array) && !Array.isArray(identityMatrix)) {
+    throw new Error('Invalid identity matrix type');
+  }
+  return identityMatrix;
 }
frontend/javascripts/oxalis/constants.ts (1)

18-19: LGTM! Consider enhancing the type documentation.

The NestedMatrix4 type addition is well-structured and appropriate for handling affine transformations.

Consider enhancing the documentation to include:

  1. Usage examples
  2. Relationship with affine transformations
  3. Expected matrix structure (e.g., rotation, translation components)
-export type NestedMatrix4 = [Vector4, Vector4, Vector4, Vector4]; // Represents a row major matrix.
+/**
+ * Represents a 4x4 row-major matrix used for affine transformations.
+ * Structure:
+ * [
+ *   [m11, m12, m13, m14], // First row: rotation + scale
+ *   [m21, m22, m23, m24], // Second row: rotation + scale
+ *   [m31, m32, m33, m34], // Third row: rotation + scale
+ *   [m41, m42, m43, m44]  // Fourth row: translation + perspective
+ * ]
+ */
+export type NestedMatrix4 = [Vector4, Vector4, Vector4, Vector4];
frontend/javascripts/oxalis/model_initialization.ts (2)

Line range hint 481-508: Consider adding error handling for the coordinate transformation logic.

While the conditional logic for applying transformations is sound, consider adding error handling for edge cases:

  1. When originalLayers is empty
  2. When the first layer's transformations are undefined
  3. When fallback layer's transformations are invalid
 const allLayersSameRotation = doAllLayersHaveTheSameRotation(originalLayers);
+if (originalLayers.length === 0) {
+  return {};
+}
 let coordinateTransformsMaybe = {};
 if (allLayersSameRotation) {
+  if (!originalLayers[0]?.coordinateTransformations) {
+    console.warn('First layer transformations undefined, using identity matrix');
+    return {};
+  }
   coordinateTransformsMaybe = {
     coordinateTransformations: originalLayers?.[0].coordinateTransformations,
   };
 } else if (fallbackLayer?.coordinateTransformations) {
+  try {
     coordinateTransformsMaybe = {
       coordinateTransformations: fallbackLayer.coordinateTransformations,
     };
+  } catch (err) {
+    console.error('Failed to apply fallback layer transformations:', err);
+    return {};
+  }
 }

856-881: Simplify the nativelyRenderedLayerName validation logic.

The current nested conditions can be simplified for better readability while maintaining the same functionality.

-  if (originalDatasetSettings.nativelyRenderedLayerName) {
-    const isNativelyRenderedNamePresent =
-      dataset.dataSource.dataLayers.some(
-        (layer) =>
-          layer.name === originalDatasetSettings.nativelyRenderedLayerName ||
-          (layer.category === "segmentation" &&
-            layer.fallbackLayer === originalDatasetSettings.nativelyRenderedLayerName),
-      ) ||
-      annotation?.annotationLayers.some(
-        (layer) => layer.name === originalDatasetSettings.nativelyRenderedLayerName,
-      );
-    if (!isNativelyRenderedNamePresent) {
-      initialDatasetSettings.nativelyRenderedLayerName = null;
-    }
-  }
+  const { nativelyRenderedLayerName } = originalDatasetSettings;
+  if (!nativelyRenderedLayerName) {
+    return initialDatasetSettings;
+  }
+
+  const isLayerInDataset = dataset.dataSource.dataLayers.some(
+    (layer) =>
+      layer.name === nativelyRenderedLayerName ||
+      (layer.category === "segmentation" && layer.fallbackLayer === nativelyRenderedLayerName)
+  );
+
+  const isLayerInAnnotation = annotation?.annotationLayers.some(
+    (layer) => layer.name === nativelyRenderedLayerName
+  );
+
+  if (!isLayerInDataset && !isLayerInAnnotation) {
+    initialDatasetSettings.nativelyRenderedLayerName = null;
+  }
frontend/javascripts/test/reducers/flycam_reducer.spec.ts (1)

39-39: Consider adding tests for dataset layer rotation interactions

Given that this PR introduces dataset layer rotation features, consider adding test cases that verify:

  1. Flycam behavior when interacting with rotated dataset layers
  2. Transformation matrix calculations with layer rotation applied
  3. Edge cases when switching between rotated and non-rotated views

Would you like me to help draft these additional test cases?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 2bdc9eb and 1cd5578.

📒 Files selected for processing (32)
  • frontend/javascripts/admin/dataset/composition_wizard/04_configure_new_dataset.tsx (1 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (1 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx (3 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (3 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx (1 hunks)
  • frontend/javascripts/libs/mjs.ts (1 hunks)
  • frontend/javascripts/oxalis/api/api_latest.ts (1 hunks)
  • frontend/javascripts/oxalis/constants.ts (1 hunks)
  • frontend/javascripts/oxalis/controller/scene_controller.ts (1 hunks)
  • frontend/javascripts/oxalis/geometries/materials/edge_shader.ts (1 hunks)
  • frontend/javascripts/oxalis/geometries/materials/node_shader.ts (1 hunks)
  • frontend/javascripts/oxalis/geometries/materials/plane_material_factory.ts (2 hunks)
  • frontend/javascripts/oxalis/merger_mode.ts (1 hunks)
  • frontend/javascripts/oxalis/model/accessors/dataset_accessor.ts (1 hunks)
  • frontend/javascripts/oxalis/model/accessors/dataset_layer_rotation_accessor.ts (1 hunks)
  • frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts (2 hunks)
  • frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts (3 hunks)
  • frontend/javascripts/oxalis/model/accessors/tool_accessor.ts (2 hunks)
  • frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts (2 hunks)
  • frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.ts (1 hunks)
  • frontend/javascripts/oxalis/model/helpers/nml_helpers.ts (1 hunks)
  • frontend/javascripts/oxalis/model/helpers/transformation_helpers.ts (2 hunks)
  • frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts (1 hunks)
  • frontend/javascripts/oxalis/model/sagas/dataset_saga.ts (1 hunks)
  • frontend/javascripts/oxalis/model/sagas/quick_select_heuristic_saga.ts (1 hunks)
  • frontend/javascripts/oxalis/model_initialization.ts (5 hunks)
  • frontend/javascripts/oxalis/store.ts (1 hunks)
  • frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (7 hunks)
  • frontend/javascripts/test/reducers/flycam_reducer.spec.ts (1 hunks)
  • frontend/javascripts/types/api_flow_types.ts (3 hunks)
  • frontend/javascripts/types/globals.d.ts (1 hunks)
  • frontend/javascripts/types/schemas/dataset_view_configuration.schema.ts (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • frontend/javascripts/oxalis/model/helpers/nml_helpers.ts
  • frontend/javascripts/oxalis/store.ts
  • frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx
  • frontend/javascripts/oxalis/api/api_latest.ts
🔇 Additional comments (30)
frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (2)

72-105: ⚠️ Potential issue

Add 'dataLayers' and 'datasetBoundingBox' to useCallback dependency array

The setMatrixRotationsForAllLayer function uses dataLayers and datasetBoundingBox obtained from form.getFieldValue and getDatasetBoundingBoxFromLayers(dataLayers). To ensure the callback updates correctly when these values change, include them in the dependency array.

Apply this diff to update the dependencies:

   }, [
     axis,
     form,
+    dataLayers,
+    datasetBoundingBox,
   ]);

Likely invalid or redundant comment.


47-70: ⚠️ Potential issue

Include 'rotationValues' in useEffect dependency array

The rotationValues variable is derived from form.getFieldValue(["datasetRotation"]) and is used within the useEffect hook. To ensure that the effect runs whenever rotationValues change, consider including rotationValues in the dependency array.

Apply this diff to include rotationValues in the dependency array:

   }, [
     datasetBoundingBox,
     dataLayers,
     form,
+    rotationValues,
   ]);

Likely invalid or redundant comment.

frontend/javascripts/admin/dataset/composition_wizard/04_configure_new_dataset.tsx (1)

Line range hint 223-225: Check for empty 'layersWithTransforms' before proceeding

After the withTransforms function, ensure that layersWithTransforms is not empty before proceeding to prevent potential runtime errors.

frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (1)

Line range hint 1185-1235: Ensure consistent rendering of 'TransformationIcon' in 'getSkeletonLayer'

When rendering the TransformationIcon within the getSkeletonLayer method, verify that the layer prop is correctly passed and that the skeleton layer supports transformations if applicable.

frontend/javascripts/types/schemas/dataset_view_configuration.schema.ts (1)

152-154: ⚠️ Potential issue

Add validation constraints for nativelyRenderedLayerName

The schema allows any string value for nativelyRenderedLayerName without validation. This could lead to runtime errors if invalid layer names are provided.

Consider adding these constraints:

 nativelyRenderedLayerName: {
   type: "string",
+  minLength: 1,
+  pattern: "^[a-zA-Z0-9_-]+$"
 },

Let's verify the layer name usage:

frontend/javascripts/oxalis/geometries/materials/edge_shader.ts (1)

14-14: Verify transformation consistency after module reorganization

The getTransformsForSkeletonLayer import has been moved to a new module. While the change improves code organization, we need to ensure transformation calculations remain consistent.

Let's verify the transformation logic:

✅ Verification successful

Based on the search results, I can now generate the final response:

Import change for getTransformsForSkeletonLayer is safe and consistent

The function is used consistently across the codebase:

  • In edge_shader.ts and node_shader.ts for shader transformations
  • In nml_helpers.ts for NML transformations
  • In skeletontracing_accessor.ts for node position transformations

All usages correctly handle the transformation matrix and follow the same pattern of accessing the dataset and natively rendered layer name from the state.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find all usages of getTransformsForSkeletonLayer to ensure consistent behavior
rg "getTransformsForSkeletonLayer" -A 5

# Look for related transformation functions
ast-grep --pattern 'function getTransformsFor$_($_, $_) {
  $$$
}'

Length of output: 7571

frontend/javascripts/oxalis/model/helpers/transformation_helpers.ts (1)

5-5: LGTM: Type imports and function signature changes enhance type safety.

The changes improve type safety by introducing the NestedMatrix4 type and updating function signatures accordingly.

Also applies to: 7-9

frontend/javascripts/oxalis/model/sagas/dataset_saga.ts (1)

15-18: LGTM: Import restructuring improves code organization.

Moving transformation-related functions to a dedicated accessor module enhances code organization and maintainability.

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

8-8: LGTM: New type import.

The import of BoundingBoxObject type is appropriate for the new functionality.

frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts (2)

26-30: LGTM: Import changes align with new transformation module

The imports have been updated to use the new transformation helpers and layer rotation accessor, improving code organization.


221-224: Verify transformation changes across the codebase

The refactoring of transformation logic looks correct, but we should verify its usage across the codebase.

Also applies to: 234-236, 241-243

✅ Verification successful

Let me gather more information about the transformation functions to ensure completeness of the verification.


Transformation functions are correctly used across the codebase

The verification shows that:

  • getTransformsForLayerWithoutTransformationConfigOrNull is used consistently in skeletontracing_accessor.ts and dataset_layer_rotation_accessor.ts
  • getTransformsForSkeletonLayer is used in shader-related files and NML helpers
  • All transformation function calls follow the same parameter pattern (dataset and nativelyRenderedLayerName)
  • No references to deprecated transformation functions were found
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for any remaining references to old transformation functions
rg -l "getTransformsForSkeletonLayerOrNull" 

# Verify all transformation function calls
ast-grep --pattern 'getTransformsForSkeletonLayer($_, $_)'

Length of output: 1980


Script:

#!/bin/bash
# Check for all transformation-related function calls
ast-grep --pattern 'getTransformsForLayerWithoutTransformationConfigOrNull($_, $_)'

# Search for any other transformation-related functions
rg -l "getTransformsFor.*Layer"

Length of output: 2028

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

105-105: LGTM: Fixed matrix initialization

The change from M4x4.identity to M4x4.identity() ensures a new matrix instance is created, preventing potential shared reference issues.

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

33-36: LGTM! Clean refactoring of transformation functions.

The relocation of transformation-related functions to a dedicated accessor module improves code organization and maintainability.

frontend/javascripts/oxalis/geometries/materials/node_shader.ts (1)

17-17: Verify skeleton layer transformations.

The import change aligns with the refactoring, but given the PR's mention of skeleton layer compatibility with transformations, we should verify that all skeleton layer transformations work correctly.

✅ Verification successful

Let me gather more information about the transformation usage to better understand the impact.


Based on the search results, I can now generate the final response:

Import change for skeleton layer transformations is correctly implemented

The getTransformsForSkeletonLayer function is properly used across the codebase:

  • Used in shader materials (node_shader.ts, edge_shader.ts) for rendering transformations
  • Integrated with NML helpers and skeleton tracing accessors
  • Proper error handling exists for transformed skeleton layers in proofreading functionality

The import change aligns with the existing usage patterns and maintains the expected transformation behavior.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for skeleton layer transformation usage and potential issues.

# Search for skeleton layer transformation tests
rg -A 5 "test.*skeleton.*transform|test.*transform.*skeleton"

# Search for skeleton layer transformation error handling
rg -A 5 "catch.*skeleton.*transform|error.*skeleton.*transform"

# Search for skeleton layer transformation edge cases
ast-grep --pattern $'getTransformsForSkeletonLayer($_, $_) {
  $$$
  if ($cond) {
    $$$
  }
  $$$
}'

Length of output: 1419


Script:

#!/bin/bash
# Search for usages of getTransformsForSkeletonLayer
rg -A 5 "getTransformsForSkeletonLayer"

# Search for skeleton layer transformation related code
ast-grep --pattern 'class $_ {
  $$$
  transform($_, $_) {
    $$$
  }
  $$$
}'

# Check for any other transformation related imports
rg "import.*transform.*dataset_layer_rotation"

Length of output: 7633

frontend/javascripts/oxalis/model/accessors/tool_accessor.ts (1)

10-11: LGTM! Verify refactoring impact.

The import changes appropriately reflect the reorganization of transformation-related functions into a dedicated accessor module, which aligns with the PR's objective of supporting dataset layer rotations.

✅ Verification successful

Import changes are correctly refactored across the codebase

The verification shows that the transformation-related functions have been consistently moved to dataset_layer_rotation_accessor. There are no remaining imports from the old location in dataset_accessor, and the new import pattern is used consistently across 11 different files in the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that all transformation-related imports are consistently updated across the codebase

# Check for any remaining old imports
rg "import.*getTransformsPerLayer.*dataset_accessor"

# Verify the new import pattern is used consistently
rg "import.*dataset_layer_rotation_accessor"

Length of output: 1818

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

10-11: LGTM! Verify transformation handling in merger mode.

The import changes ensure proper handling of layer transformations in merger mode, maintaining correct coordinate mapping through inverse transformations.

✅ Verification successful

LGTM! Transformation handling in merger mode is correctly implemented.

The code shows proper usage of the imported transformation functions:

  • getInverseSegmentationTransformer is used in two places (lines 203 and 370) to correctly transform node positions before looking up segment IDs
  • The transformation flow maintains coordinate mapping integrity:
    1. Node positions are first transformed to rendered coordinates
    2. Inverse segmentation transform is applied to get the correct lookup position
    3. Segment IDs are retrieved using the transformed coordinates
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that merger mode correctly handles transformed layers

# Check for proper transformation handling in merger mode
rg -A 5 "getInverseSegmentationTransformer.*merger_mode"

# Verify no direct access to transformation functions from dataset_accessor
rg "import.*getTransformsPerLayer.*dataset_accessor.*merger_mode"

Length of output: 126


Script:

#!/bin/bash
# Let's search for usage of the imported functions in merger_mode.ts
ast-grep --pattern 'getInverseSegmentationTransformer($$$)'

# Also check for any usage of getVisibleSegmentationLayer
ast-grep --pattern 'getVisibleSegmentationLayer($$$)'

# Let's see the full content of merger_mode.ts to understand the context better
cat frontend/javascripts/oxalis/merger_mode.ts

Length of output: 20557

frontend/javascripts/oxalis/controller/scene_controller.ts (3)

49-49: LGTM: Clean import addition

The import of getTransformsForLayerOrNull is well-placed and follows the module structure.


Line range hint 408-411: LGTM: Improved null check for dataLayers

The null check for dataLayers has been improved to prevent potential runtime errors when filtering layer names.


Line range hint 203-225: LGTM: Well-implemented transformation logic

The transformation application logic is well-structured:

  1. Correctly retrieves transformations for each layer
  2. Properly applies the affine transformation matrix using Three.js Matrix4
  3. Maintains the layer visibility state
frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx (2)

36-36: LGTM: Clean import addition

The import of AxisRotationSettingForDataset is well-placed and follows the module structure.


271-276: LGTM: Well-structured UI layout

The rotation settings UI is well-integrated:

  1. Follows Ant Design's Row/Col pattern for responsive layout
  2. Appropriate placement within the form structure
  3. Clear separation of concerns with dedicated column for rotation settings
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (3)

46-50: LGTM: Clean type imports and declarations

The imports and type declarations are well-organized and properly documented.


84-84: LGTM: Clean type extension

The FormData type is appropriately extended with the optional datasetRotation field.


203-225: LGTM: Well-implemented rotation settings initialization

The rotation settings initialization logic is robust:

  1. Properly checks for consistent rotation across layers
  2. Handles missing transformations gracefully with default values
  3. Correctly extracts rotation values from transformation matrices
  4. Clear comments explaining the transformation sequence
frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts (1)

196-196: LGTM! Proper initialization of identity matrix.

The change correctly ensures that M4x4.identity() is called as a function rather than accessing it as a property.

frontend/javascripts/oxalis/model/sagas/quick_select_heuristic_saga.ts (1)

61-61: LGTM! Improved module organization.

The import has been correctly updated to use the new dedicated dataset_layer_rotation_accessor module, which better organizes transformation-related functions.

frontend/javascripts/types/api_flow_types.ts (2)

63-73: Well-structured type definitions for transformations.

The separation of transformation types and explicit documentation of matrix storage order (row-major) improves type safety and prevents potential matrix operation errors.


99-100: Clear documentation for APISkeletonLayer name property.

The comment effectively explains that the name should be the skeleton tracing ID to ensure uniqueness.

frontend/javascripts/oxalis/geometries/materials/plane_material_factory.ts (1)

53-57: Good modularization of transformation-related functions.

Moving transformation functions to a dedicated accessor module improves code organization and maintainability.

frontend/javascripts/test/reducers/flycam_reducer.spec.ts (1)

39-39: Good change: Ensures proper matrix initialization

The change from M4x4.identity to M4x4.identity() ensures each test gets a fresh identity matrix instance, preventing potential mutation issues between tests.

Comment on lines 107 to 135
<Row gutter={24}>
<Col span={16}>
<FormItemWithInfo
name={["datasetRotation", axis]}
label={`${axis.toUpperCase()} Axis Rotation`}
info={`Change the datasets rotation around the ${axis}-axis.`}
colon={false}
>
<Slider min={0} max={270} step={90} onChange={setMatrixRotationsForAllLayer} />
</FormItemWithInfo>
</Col>
<Col span={8} style={{ marginRight: -12 }}>
<FormItem
name={["datasetRotation", axis]}
colon={false}
label=" " /* Whitespace label is needed for correct formatting*/
>
<InputNumber
min={0}
max={270}
step={90}
precision={0}
onChange={(value: number | null) => value && setMatrixRotationsForAllLayer(value)}
/>
</FormItem>
</Col>
</Row>
);
};
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

Handle null 'value' in InputNumber onChange handler

In the InputNumber component, the onChange handler checks value && setMatrixRotationsForAllLayer(value), which will ignore 0 since 0 is a falsy value. Use a more explicit null check to ensure that zero values are handled correctly.

Apply this diff to fix the condition:

     <InputNumber
       min={0}
       max={270}
       step={90}
       precision={0}
-      onChange={(value: number | null) => value && setMatrixRotationsForAllLayer(value)}
+      onChange={(value: number | null) => {
+        if (value !== null) {
+          setMatrixRotationsForAllLayer(value);
+        }
+      }}
     />
📝 Committable suggestion

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

Suggested change
<Row gutter={24}>
<Col span={16}>
<FormItemWithInfo
name={["datasetRotation", axis]}
label={`${axis.toUpperCase()} Axis Rotation`}
info={`Change the datasets rotation around the ${axis}-axis.`}
colon={false}
>
<Slider min={0} max={270} step={90} onChange={setMatrixRotationsForAllLayer} />
</FormItemWithInfo>
</Col>
<Col span={8} style={{ marginRight: -12 }}>
<FormItem
name={["datasetRotation", axis]}
colon={false}
label=" " /* Whitespace label is needed for correct formatting*/
>
<InputNumber
min={0}
max={270}
step={90}
precision={0}
onChange={(value: number | null) => value && setMatrixRotationsForAllLayer(value)}
/>
</FormItem>
</Col>
</Row>
);
};
<Row gutter={24}>
<Col span={16}>
<FormItemWithInfo
name={["datasetRotation", axis]}
label={`${axis.toUpperCase()} Axis Rotation`}
info={`Change the datasets rotation around the ${axis}-axis.`}
colon={false}
>
<Slider min={0} max={270} step={90} onChange={setMatrixRotationsForAllLayer} />
</FormItemWithInfo>
</Col>
<Col span={8} style={{ marginRight: -12 }}>
<FormItem
name={["datasetRotation", axis]}
colon={false}
label=" " /* Whitespace label is needed for correct formatting*/
>
<InputNumber
min={0}
max={270}
step={90}
precision={0}
onChange={(value: number | null) => {
if (value !== null) {
setMatrixRotationsForAllLayer(value);
}
}}
/>
</FormItem>
</Col>
</Row>
);
};

Comment on lines 75 to 90
const matrix = transformation ? transformation.matrix : IDENTITY_MATRIX;
const cosineLocation = cosineLocationOfRotationInMatrix[axis];
const sinusLocation = sinusLocationOfRotationInMatrix[axis];
const sinOfAngle = matrix[sinusLocation[0]][sinusLocation[1]];
const cosOfAngle = matrix[cosineLocation[0]][cosineLocation[1]];
const rotation =
Math.abs(cosOfAngle) > 1e-6 // Avoid division by zero
? Math.atan2(sinOfAngle, cosOfAngle)
: sinOfAngle > 0
? Math.PI / 2
: -Math.PI / 2;
const rotationInDegrees = rotation * (180 / Math.PI);
// Round to multiple of 90 degrees and keep the result positive.
const roundedRotation = mod(Math.round((rotationInDegrees + 360) / 90) * 90, 360);
return roundedRotation;
}
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

Handle division by zero in 'getRotationFromTransformation'

The function getRotationFromTransformation includes a check to avoid division by zero when cosOfAngle is close to zero. However, the condition Math.abs(cosOfAngle) > 1e-6 might not be sufficient. Consider using a stricter threshold or handling the edge case more robustly.

Copy link
Contributor Author

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

#rotate

@philippotto This PR is ready for the first review round. To you have time to take on this task or should I ask daniel? Because I think you are already packed with the Unified Annotation Versioning but as you built the coordinateTransforms & multi modality feature you might be a little more suited for the review 🤔

Comment on lines 247 to 260
return null;
}

// The skeleton layer needs transformed just like the other layers. Thus, we simply use the first usable layer.
// Filtering for a layer that might actually have transforms prevents an infinite loop
// between cyclic calls of _getTransformsForLayerWithoutTransformationConfigOrNull and getTransformsForLayerOrNull.
const usableReferenceLayer = layers.find(
(layer) =>
layer.category === "color" || (layer.category === "segmentation" && layer.fallbackLayer),
);
const someLayersTransformsMaybe = usableReferenceLayer
? getTransformsForLayerOrNull(dataset, usableReferenceLayer, nativelyRenderedLayerName)
: null;
return someLayersTransformsMaybe;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The filtering here avoids a potential infinite loop in the following scenario:

In the scenario the user has an annotation with no color layers and just two volume layers with fallback segmentation. For this DS a rotation is defined.
Now in case the user detaches the first volume layer from its segmentation layer, the layer has no transforms anymore. => This _getTransformsForLayerWithoutTransformationConfigOrNull function will be called for this layer without the fallback segmentation and thus no transformation.
=> If we would not filter for a suitable layer and instead take e.g. the first layer via dataset.dataSource.layers[0] we would get the same layer again and then call getTransformsForLayerOrNull which then automatically calls _getTransformsForLayerWithoutTransformationConfigOrNull again, because the layer fallback segmentation. And thus would have a cyclic call in this scenario.

Therefore, I think the filter should avoid a potential infinite loop.

frontend/javascripts/libs/mjs.ts Show resolved Hide resolved
Comment on lines 499 to 508
let coordinateTransformsMaybe = {};
if (doAllLayersHaveTheSameRotation) {
coordinateTransformsMaybe = {
coordinateTransformations: originalLayers?.[0].coordinateTransformations,
};
} else if (fallbackLayer?.coordinateTransformations) {
coordinateTransformsMaybe = {
coordinateTransformations: fallbackLayer.coordinateTransformations,
};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here transfer the coordinateTransforms from the fallback layer to its volume annotation layer in order to not loose these settings

@philippotto
Copy link
Member

@philippotto This PR is ready for the first review round. To you have time to take on this task or should I ask daniel?

Yes, I can review it :)

Copy link
Member

@philippotto philippotto left a comment

Choose a reason for hiding this comment

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

cool stuff! I already left some couple, but I'm not done with all files yet.

@@ -94,8 +96,8 @@ export type APISegmentationLayer = APIDataLayerBase & {
export type APIDataLayer = APIColorLayer | APISegmentationLayer;

// Only used in rare cases to generalize over actual data layers and
// a skeleton layer.
export type APISkeletonLayer = { category: "skeleton" };
// a skeleton layer. The name should be the skeleton tracing id to very likely ensure it is unique.
Copy link
Member

Choose a reason for hiding this comment

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

why only very likely? because a dataset layer name could be equal to the skeleton id? but then the category wouldn't be right, would it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All yes, but the layer name is used to save which layer is currently rendered natively. This is not supported for the skeleton layer as the skeleton layer has no rotation settings.

But in case a volume layer is currently rendered natively and has the same name as the skeleton layer's tracing id, this would lead to unwanted behaviour in _getTransformsForLayerOrNull. The skeleton layer would be treated as the layer rendered natively and thus would not have transforms instead of transforming into the coordinate system of the volume layer

frontend/javascripts/oxalis/store.ts Outdated Show resolved Hide resolved
// so that the user will still look at the same data location.
// Get transform of layer. null is passed as nativelyRenderedLayerName to
// get the layers transform even in case the is currently rendered natively.
const layersTransforms = getTransformsForLayer(state.dataset, layer, null);
Copy link
Member

Choose a reason for hiding this comment

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

on master, there is _getOriginalTransformsForLayerOrNull. can that be reused? I think, I like it better to call a method which uses "original transforms" (or a similar term) compared to passing null for the last arg.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, awesome, thanks for the suggestion. The advantage of using getTransformsForLayer was that it could handle all kinda of layers. Also layers that do not support configuring a transformation (isLayerWithoutTransformationConfigSupport). But this should not happen as in that case the button is disabled. Therefore, I the code now uses getOriginalTransformsForLayerOrNull as suggested by you :)

Comment on lines 264 to 269
const transformWhichWillBeApplied = !isRenderedNatively
? invertTransform(layersTransforms)
: layersTransforms;

const currentPosition = getPosition(state.flycam);
const currentTransforms = getTransformsForLayer(
state.dataset,
layer,
state.datasetConfiguration.nativelyRenderedLayerName,
);
const invertedTransform = invertTransform(currentTransforms);
const newPosition = transformPointUnscaled(invertedTransform)(currentPosition);
const newPosition = transformPointUnscaled(transformWhichWillBeApplied)(currentPosition);
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit confused. Here's my understanding of the old and the new behavior (I might be wrong, of course).

Setup:

  • DS has two layers. L1 has no transforms. L2 has a translation transform that moves the entire layer by 1000vx in X (let's call that T).
  • I open the DS and select that L2 is rendered natively. Therefore, L1 will be rendered with an -1000vx offset in X (T_inv).
  • the current position is at 0,0,0
  • Now I select that L1 should be rendered natively -> layer will refer to L1

Old behavior:

  • currentTransforms = T_inv
  • invertedTransforms = T
  • newPosition = 1000,0,0

New behavior:

  • layersTransforms = identity (because L1 has no transforms in DS)
  • transformWhichWillBeApplied = identity
  • newPosition = 0,0,0

the new behavior seems wrong to me? am I missing something?

}

export function isIdentityTransform(transform: Transform) {
return transform.type === "affine" && _.isEqual(transform.affineMatrix, Identity4x4);
Copy link
Member

Choose a reason for hiding this comment

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

we don't need to use Number.EPSILON when comparing affineMatrix to the identity matrix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In getRotationMatrixAroundAxis (which generates the problematic matrixes with very small values close to 0) I replace all very small values (Math.abs(value) < Number.EPSILON) to 0 to avoid the bug overall. This makes the saved matrix in the datasource-properties.json also easier to read.

/* This function checks if all layers have the same transformation settings that represent
* a translation to the dataset center and a rotation around each axis and a translation back.
* All together this makes 5 affine transformation matrices. */
function _doAllLayersHaveTheSameRotation(dataLayers: Array<APIDataLayer>): boolean {
Copy link
Member

Choose a reason for hiding this comment

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

maybe we can discuss this in person. I don't understand how this fits into the bigger picture yet.

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

🧹 Nitpick comments (6)
frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (1)

257-273: Consider improving variable names and documentation

While the transformation logic is correct, readability could be improved:

  1. Variable layersTransforms could be renamed to originalLayerTransform to better reflect its purpose
  2. Consider adding a comment explaining why the transform needs to be inverted when not rendered natively
-    const layersTransforms =
-      getOriginalTransformsForLayerOrNull(state.dataset, layer) || IdentityTransform;
+    // Get the layer's original transformation matrix, defaulting to identity if none exists
+    const originalLayerTransform =
+      getOriginalTransformsForLayerOrNull(state.dataset, layer) || IdentityTransform;

-    const transformWhichWillBeApplied = !isRenderedNatively
-      ? invertTransform(layersTransforms)
-      : layersTransforms;
+    // When switching from transformed to native rendering, we need to apply the inverse transform
+    // to maintain the correct view position
+    const transformWhichWillBeApplied = !isRenderedNatively
+      ? invertTransform(originalLayerTransform)
+      : originalLayerTransform;
CHANGELOG.unreleased.md (1)

14-14: Consider enhancing the changelog entry with more details.

The changelog entry could be more informative by mentioning that rotations are limited to 90-degree steps and any specific limitations or requirements (e.g., all layers must have the same rotation).

-Added the possibility to configure a rotation for a dataset which can be toggled off and on when viewing and annotating data. [#8159](https://github.com/scalableminds/webknossos/pull/8159)
+Added the possibility to configure dataset rotation in 90-degree steps around each axis, which can be toggled off and on when viewing and annotating data. Note: All layers must have the same rotation settings. [#8159](https://github.com/scalableminds/webknossos/pull/8159)
frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (3)

47-70: Consider adding useCallback for the useEffect dependency.

The useEffect hook depends on the form instance which could lead to unnecessary re-renders. Consider memoizing the form operations.

+const updateTransformations = useCallback((
+  datasetBoundingBox: BoundingBox,
+  dataLayers: APIDataLayer[],
+  form: FormInstance,
+) => {
+  const rotationValues = form.getFieldValue(["datasetRotation"]);
+  const transformations = [
+    fromCenterToOrigin(datasetBoundingBox),
+    getRotationMatrixAroundAxis("x", rotationValues["x"]),
+    getRotationMatrixAroundAxis("y", rotationValues["y"]),
+    getRotationMatrixAroundAxis("z", rotationValues["z"]),
+    fromOriginToCenter(datasetBoundingBox),
+  ];
+  const dataLayersWithUpdatedTransforms = dataLayers.map((layer) => ({
+    ...layer,
+    coordinateTransformations: transformations,
+  }));
+  form.setFieldValue(["dataSource", "dataLayers"], dataLayersWithUpdatedTransforms);
+}, []);

 useEffect(() => {
   if (
     datasetBoundingBox == null ||
     dataLayers[0].coordinateTransformations?.length !== 5 ||
     !form
   ) {
     return;
   }
-  const rotationValues = form.getFieldValue(["datasetRotation"]);
-  const transformations = [
-    fromCenterToOrigin(datasetBoundingBox),
-    getRotationMatrixAroundAxis("x", rotationValues["x"]),
-    getRotationMatrixAroundAxis("y", rotationValues["y"]),
-    getRotationMatrixAroundAxis("z", rotationValues["z"]),
-    fromOriginToCenter(datasetBoundingBox),
-  ];
-  const dataLayersWithUpdatedTransforms = dataLayers.map((layer) => ({
-    ...layer,
-    coordinateTransformations: transformations,
-  }));
-  form.setFieldValue(["dataSource", "dataLayers"], dataLayersWithUpdatedTransforms);
+  updateTransformations(datasetBoundingBox, dataLayers, form);
 }, [datasetBoundingBox, dataLayers, form, updateTransformations]);

159-171: Consider extracting tooltip content to a constant.

The tooltip content is quite long and could be reused. Consider extracting it to a constant for better maintainability.

+const ROTATION_REQUIREMENTS_TOOLTIP = (
+  <div>
+    Each layers transformations must be equal and each layer needs exactly 5 affine
+    transformation with the following schema:
+    <ul>
+      <li>Translation to the origin</li>
+      <li>Rotation around the x-axis</li>
+      <li>Rotation around the y-axis</li>
+      <li>Rotation around the z-axis</li>
+      <li>Translation back to the original position</li>
+    </ul>
+    To easily enable this setting, delete all coordinateTransformations of all layers in the
+    advanced tab, save and reload the dataset settings.
+  </div>
+);

 <Tooltip
-  title={
-    <div>
-      Each layers transformations must be equal and each layer needs exactly 5 affine
-      transformation with the following schema:
-      <ul>
-        <li>Translation to the origin</li>
-        <li>Rotation around the x-axis</li>
-        <li>Rotation around the y-axis</li>
-        <li>Rotation around the z-axis</li>
-        <li>Translation back to the original position</li>
-      </ul>
-      To easily enable this setting, delete all coordinateTransformations of all layers in the
-      advanced tab, save and reload the dataset settings.
-    </div>
-  }
+  title={ROTATION_REQUIREMENTS_TOOLTIP}

174-177: Improve the error message clarity.

The current error message could be more user-friendly by providing clearer instructions.

-Setting a dataset's rotation is only supported when all layers have the same rotation
-transformation. <InfoCircleOutlined />
+Dataset rotation requires consistent transformation settings across all layers. Please ensure all layers
+have matching rotation configurations before proceeding. <InfoCircleOutlined />
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (1)

203-225: Consider architectural implications of rotation implementation

Based on the PR objectives, there are several important architectural considerations:

  1. The interface being "under design" suggests we should make the rotation implementation flexible for future UI changes
  2. The transformation sequence (translation to origin → rotate → translate back) correctly handles rotation around dataset's center
  3. Using fixed 90-degree steps mitigates the non-deterministic nature of Euler angles, but this constraint should be documented

Consider:

  1. Adding an interface abstraction layer to decouple rotation logic from UI
  2. Documenting the 90-degree step constraint in the API documentation
  3. Adding unit tests to verify rotation behavior around dataset's center
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1cd5578 and 9c08ab0.

📒 Files selected for processing (8)
  • CHANGELOG.unreleased.md (1 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (1 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx (2 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (3 hunks)
  • frontend/javascripts/oxalis/model/accessors/dataset_layer_rotation_accessor.ts (1 hunks)
  • frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts (3 hunks)
  • frontend/javascripts/oxalis/store.ts (1 hunks)
  • frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/javascripts/oxalis/store.ts
  • frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx
  • frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts
🔇 Additional comments (10)
frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (5)

87-87: LGTM: New imports support rotation functionality

The added imports from dataset layer rotation accessor and transformation helpers modules appropriately support the new rotation functionality requirements.

Also applies to: 127-138


228-230: LGTM: Robust validation for transformation toggles

The implementation correctly prevents invalid transformation states by:

  1. Checking if all layers have the same rotation
  2. Preventing toggling on layers that cannot have transformations (skeleton layers and segmentation layers without fallback)
  3. Preventing toggling when it would affect other layers' coordinate systems

Also applies to: 234-234, 247-254


295-302: LGTM: Clear visual feedback for interaction states

The styling changes appropriately communicate:

  1. Interactive state through cursor styles
  2. Disabled state through opacity

308-312: LGTM: Clear user feedback for transformation states

The tooltip text and image source changes effectively communicate:

  1. Whether the layer is shown natively or with transformations
  2. The type of transformation being applied
  3. Whether the transformation can be toggled

Also applies to: 316-319


1189-1189: LGTM: Proper skeleton layer identification

Added tracingId to skeleton layer object to ensure proper transformation handling.

Also applies to: 1239-1239

frontend/javascripts/oxalis/model/accessors/dataset_layer_rotation_accessor.ts (2)

83-87: ⚠️ Potential issue

Enhance division by zero handling.

The current threshold for avoiding division by zero might not be sufficient in all cases. Consider using a more robust approach.

-  Math.abs(cosOfAngle) > 1e-6 // Avoid division by zero
+  Math.abs(cosOfAngle) > Number.EPSILON * 100 // Use a more robust threshold for floating-point comparison

Likely invalid or redundant comment.


335-337: ⚠️ Potential issue

Optimize memoization strategy.

The current memoization using _.memoize might lead to memory leaks as it doesn't have a cache size limit. Consider using a more robust memoization approach.

-export const invertAndTranspose = _.memoize((mat: Matrix4x4) => {
-  return M4x4.transpose(M4x4.inverse(mat));
-});
+const memoizedInvertAndTranspose = new Map<string, Matrix4x4>();
+const MAX_CACHE_SIZE = 1000;
+
+export const invertAndTranspose = (mat: Matrix4x4) => {
+  const key = JSON.stringify(mat);
+  if (!memoizedInvertAndTranspose.has(key)) {
+    if (memoizedInvertAndTranspose.size >= MAX_CACHE_SIZE) {
+      // Clear the oldest entry if cache is full
+      const firstKey = memoizedInvertAndTranspose.keys().next().value;
+      memoizedInvertAndTranspose.delete(firstKey);
+    }
+    memoizedInvertAndTranspose.set(key, M4x4.transpose(M4x4.inverse(mat)));
+  }
+  return memoizedInvertAndTranspose.get(key)!;
+};

Likely invalid or redundant comment.

frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (3)

46-50: LGTM! Well-organized imports with good separation of concerns.

The rotation-related functionality is properly modularized in a dedicated accessor module, following good architectural practices.


84-84: Verify the DatasetRotation type definition

The optional datasetRotation field is correctly added. Let's verify the type definition to ensure it properly constrains rotation values.

✅ Verification successful

DatasetRotation type is well-defined with proper constraints

The type definition is found in dataset_rotation_form_item.tsx and correctly defines the expected rotation properties:

  • x: number
  • y: number
  • z: number
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the DatasetRotation type definition and its usage
# Expected: Type should define x, y, z rotation properties with appropriate constraints

ast-grep --pattern 'type DatasetRotation = {
  $$$
}'

# Also check for any validation logic
rg -A 5 'DatasetRotation.*validation'

Length of output: 539


203-225: 🛠️ Refactor suggestion

Improve rotation initialization robustness and documentation

The rotation initialization logic has several areas for improvement:

  1. The magic number 5 in the transformation length check needs explanation
  2. Missing validation for rotation values
  3. The transformation sequence (origin translation → rotations → position restoration) needs better documentation
  4. No error handling for malformed transformation data

Consider these improvements:

 // Retrieve the initial dataset rotation settings from the data source config.
 if (doAllLayersHaveTheSameRotation(dataSource.dataLayers)) {
   const firstLayerTransformations = dataSource.dataLayers[0].coordinateTransformations;
+  // Expected transformation sequence:
+  // 1. Translation to origin
+  // 2-4. Rotations around x, y, z axes
+  // 5. Translation back to original position
+  const EXPECTED_TRANSFORMATION_COUNT = 5;
   let initialDatasetRotationSettings: DatasetRotation;
-  if (!firstLayerTransformations || firstLayerTransformations.length !== 5) {
+  if (!firstLayerTransformations || firstLayerTransformations.length !== EXPECTED_TRANSFORMATION_COUNT) {
     initialDatasetRotationSettings = {
       x: 0,
       y: 0,
       z: 0,
     };
   } else {
+    try {
       initialDatasetRotationSettings = {
-        // First transformation is a translation to the coordinate system origin.
         x: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[1], "x"),
         y: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[2], "y"),
         z: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[3], "z"),
-        // Fifth transformation is a translation back to the original position.
       };
+    } catch (error) {
+      console.error("Failed to parse rotation transformations:", error);
+      initialDatasetRotationSettings = { x: 0, y: 0, z: 0 };
+    }
   }

Let's verify the rotation value constraints:

Comment on lines 371 to 413
function _doAllLayersHaveTheSameRotation(dataLayers: Array<APIDataLayer>): boolean {
const firstDataLayerTransformations = dataLayers[0]?.coordinateTransformations;
if (firstDataLayerTransformations == null || firstDataLayerTransformations.length === 0) {
// No transformations in all layers compatible with setting a rotation for the whole dataset.
return dataLayers.every(
(layer) =>
layer.coordinateTransformations == null || layer.coordinateTransformations.length === 0,
);
}
// There should be a translation to the origin, one transformation for each axis and one translation back. => A total of 5 affine transformations.
if (
dataLayers.some((layer) => layer.coordinateTransformations?.length !== 5) ||
dataLayers.some((layer) =>
layer.coordinateTransformations?.some((transformation) => transformation.type !== "affine"),
)
) {
return false;
}

if (
!isTranslationOnly(firstDataLayerTransformations[0] as AffineTransformation) ||
!isRotationOnly(firstDataLayerTransformations[1] as AffineTransformation) ||
!isRotationOnly(firstDataLayerTransformations[2] as AffineTransformation) ||
!isRotationOnly(firstDataLayerTransformations[3] as AffineTransformation) ||
!isTranslationOnly(firstDataLayerTransformations[4] as AffineTransformation)
) {
return false;
}
for (let i = 1; i < dataLayers.length; i++) {
const transformations = dataLayers[i].coordinateTransformations;
if (
transformations == null ||
// Not checking matrix 0 and 4 for equality as these are transformations depending on the layer's bounding box.
// The bounding box can be different for each layer.
!_.isEqual(transformations[1], firstDataLayerTransformations[1]) ||
!_.isEqual(transformations[2], firstDataLayerTransformations[2]) ||
!_.isEqual(transformations[3], firstDataLayerTransformations[3])
) {
return false;
}
}
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider breaking down the complex validation function.

The _doAllLayersHaveTheSameRotation function is quite complex and could benefit from being broken down into smaller, more focused functions.

+function hasValidTransformationCount(dataLayers: Array<APIDataLayer>): boolean {
+  return !dataLayers.some((layer) => layer.coordinateTransformations?.length !== 5);
+}

+function hasOnlyAffineTransformations(dataLayers: Array<APIDataLayer>): boolean {
+  return !dataLayers.some((layer) =>
+    layer.coordinateTransformations?.some((transformation) => transformation.type !== "affine")
+  );
+}

+function hasValidTransformationPattern(transformations: CoordinateTransformation[]): boolean {
+  return (
+    isTranslationOnly(transformations[0] as AffineTransformation) &&
+    isRotationOnly(transformations[1] as AffineTransformation) &&
+    isRotationOnly(transformations[2] as AffineTransformation) &&
+    isRotationOnly(transformations[3] as AffineTransformation) &&
+    isTranslationOnly(transformations[4] as AffineTransformation)
+  );
+}

 function _doAllLayersHaveTheSameRotation(dataLayers: Array<APIDataLayer>): boolean {
   const firstDataLayerTransformations = dataLayers[0]?.coordinateTransformations;
   if (firstDataLayerTransformations == null || firstDataLayerTransformations.length === 0) {
     return dataLayers.every(
       (layer) =>
         layer.coordinateTransformations == null || layer.coordinateTransformations.length === 0,
     );
   }

-  if (
-    dataLayers.some((layer) => layer.coordinateTransformations?.length !== 5) ||
-    dataLayers.some((layer) =>
-      layer.coordinateTransformations?.some((transformation) => transformation.type !== "affine"),
-    )
-  ) {
+  if (!hasValidTransformationCount(dataLayers) || !hasOnlyAffineTransformations(dataLayers)) {
     return false;
   }

-  if (
-    !isTranslationOnly(firstDataLayerTransformations[0] as AffineTransformation) ||
-    !isRotationOnly(firstDataLayerTransformations[1] as AffineTransformation) ||
-    !isRotationOnly(firstDataLayerTransformations[2] as AffineTransformation) ||
-    !isRotationOnly(firstDataLayerTransformations[3] as AffineTransformation) ||
-    !isTranslationOnly(firstDataLayerTransformations[4] as AffineTransformation)
-  ) {
+  if (!hasValidTransformationPattern(firstDataLayerTransformations)) {
     return false;
   }

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

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

🧹 Nitpick comments (6)
frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (1)

261-274: Consider refactoring transformation logic

The transformation logic is complex and could benefit from:

  1. Breaking down into smaller, focused functions
  2. Adding comments explaining the mathematical transformations
  3. Adding error handling for edge cases

Consider refactoring into a dedicated transformation handler:

+const calculateNewPosition = (
+  currentPosition: Vector3,
+  layersTransforms: Transform,
+  isRenderedNatively: boolean
+): Vector3 => {
+  const transformWhichWillBeApplied = !isRenderedNatively
+    ? invertTransform(layersTransforms)
+    : layersTransforms;
+  return transformPointUnscaled(transformWhichWillBeApplied)(currentPosition);
+};

 const toggleLayerTransforms = () => {
   if (layer.category === "skeleton") {
     return;
   }
   const state = Store.getState();
   const layersTransforms =
     getOriginalTransformsForLayerOrNull(state.dataset, layer) || IdentityTransform;

-  const transformWhichWillBeApplied = !isRenderedNatively
-    ? invertTransform(layersTransforms)
-    : layersTransforms;
-  const newPosition = transformPointUnscaled(transformWhichWillBeApplied)(currentPosition);
+  const currentPosition = getPosition(state.flycam);
+  const newPosition = calculateNewPosition(
+    currentPosition,
+    layersTransforms,
+    isRenderedNatively
+  );
frontend/javascripts/oxalis/model/accessors/dataset_layer_rotation_accessor.ts (5)

28-33: Consider moving the type assertion to the declaration

The type assertion can be more effectively placed at the declaration level for better type inference.

-const IDENTITY_MATRIX = [
+const IDENTITY_MATRIX: NestedMatrix4 = [
   [1, 0, 0, 0],
   [0, 1, 0, 0],
   [0, 0, 1, 0],
   [0, 0, 0, 1],
-] as NestedMatrix4;
+];

83-84: Extract magic number to a named constant

The threshold 1e-6 should be extracted to a named constant for better maintainability and clarity.

+const COSINE_ANGLE_THRESHOLD = 1e-6;
+
 const rotation =
-  Math.abs(cosOfAngle) > 1e-6 // Avoid division by zero
+  Math.abs(cosOfAngle) > COSINE_ANGLE_THRESHOLD // Avoid division by zero
     ? Math.atan2(sinOfAngle, cosOfAngle)

123-145: Add explicit return type annotations to memoization functions

Adding explicit return type annotations would improve type safety and code clarity.

-function memoizeWithThreeKeys<A, B, C, T>(fn: (a: A, b: B, c: C) => T) {
+function memoizeWithThreeKeys<A, B, C, T>(fn: (a: A, b: B, c: C) => T): (a: A, b: B, c: C) => T {

-function memoizeWithTwoKeys<A, B, T>(fn: (a: A, b: B) => T) {
+function memoizeWithTwoKeys<A, B, T>(fn: (a: A, b: B) => T): (a: A, b: B) => T {

169-172: Enhance error logging with more context

The error message could be more helpful by including the actual transform type that was encountered.

 console.error(
-  "Data layer has defined a coordinate transform that is not affine or thin_plate_spline. This is currently not supported and ignored",
+  `Data layer has defined a coordinate transform of type "${type}" that is not supported (only affine or thin_plate_spline are supported). This transform will be ignored.`,
 );

336-338: Consider using a more appropriate memoization strategy for matrix operations

The current use of _.memoize might lead to memory leaks as it doesn't have a cache size limit and uses reference equality. Consider using a custom memoization strategy with:

  1. A fixed cache size
  2. Deep equality comparison for matrices
  3. Cache eviction policy
-export const invertAndTranspose = _.memoize((mat: Matrix4x4) => {
+const matrixCache = new LRUCache<string, Matrix4x4>({ max: 100 });
+export const invertAndTranspose = (mat: Matrix4x4) => {
+  const key = JSON.stringify(mat);
+  let result = matrixCache.get(key);
+  if (!result) {
+    result = M4x4.transpose(M4x4.inverse(mat));
+    matrixCache.set(key, result);
+  }
+  return result;
-});
+};
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c08ab0 and 868585c.

📒 Files selected for processing (2)
  • frontend/javascripts/oxalis/model/accessors/dataset_layer_rotation_accessor.ts (1 hunks)
  • frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (9 hunks)
🔇 Additional comments (5)
frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (5)

127-137: LGTM: New imports for transformation handling

The added imports for transformation-related functionality align well with the PR's objective of enabling dataset layer rotation.


226-228: LGTM: Improved transformation state management

The addition of hasLayerTransformsConfigured and the updated native rendering check improve the handling of layer transformations.

Also applies to: 233-233


246-255: LGTM: Comprehensive disabled state handling

The disabled state logic properly handles various cases:

  1. Layers without transforms
  2. Skeleton layers
  3. Volume tracing layers without fallback

299-306: LGTM: Improved UI feedback for transformations

The UI changes provide better visual feedback:

  1. Clear cursor states for disabled/enabled transformations
  2. Informative tooltips explaining the transformation state

Also applies to: 312-316


1243-1243: LGTM: Proper skeleton layer transformation handling

The skeleton layer now correctly integrates with the transformation system while maintaining its special handling requirements.

Comment on lines 278 to 279
// TODOM: Fix behaviour is broken!!!

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

Address TODO comments

There are two TODO comments indicating broken behavior that need to be addressed:

  1. "Fix behaviour is broken!!!"
  2. "Make the identification what transformation will be applied to keep the position an own function"

Would you like help implementing these fixes? I can help:

  1. Debug and fix the broken behavior
  2. Refactor the transformation logic into a separate function

Michael Büßemeyer added 2 commits December 19, 2024 14:04
- fix & refactor positional change when toggling natively rendered layer
- only toggling natively rendered layer name when layer is rendered natively and has not transforms configured
@MichaelBuessemeyer
Copy link
Contributor Author

@philippotto my newest changes should implement discussed changes and also fix the new position calculation after toggling which layer is rendered natively. Please have another look at my changes 🙏

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 (11)
frontend/javascripts/oxalis/model/sagas/quick_select_heuristic_saga.ts (1)

Line range hint 632-701: Consider optimizing flood fill for large datasets.

The flood fill implementation uses a simple queue-based approach. For large datasets, consider:

  1. Using a more efficient data structure for the queue
  2. Implementing batch processing for better memory usage
  3. Adding progress tracking for long-running operations
 function fillHolesInPlace(arr: ndarray.NdArray<Uint8Array>) {
+  // Add size check for large arrays
+  const BATCH_SIZE = 1000;
+  const isLargeArray = arr.size > 1000000;
+
   const visitedField = ndarray(new Uint8Array(arr.size), arr.shape);
-  const queue: Vector2[] = [[0, 0]];
+  // Use a more efficient queue implementation for large arrays
+  const queue = isLargeArray 
+    ? new FastQueue<Vector2>() 
+    : [[0, 0]];
frontend/javascripts/oxalis/model/accessors/dataset_layer_transformation_accessor.ts (3)

75-99: Consider a clearer approach to partial rotations.

The logic in getRotationFromTransformationIn90DegreeSteps() (lines 75-99) assumes only a single-axis rotation in 90-degree increments. If additional transformations (e.g., combined rotations around multiple axes) are added in the future, this approach might become confusing or less robust. Consider handling multi-axis rotations or validating that only one axis is rotated for clarity and maintainability.


154-182: Graceful handling of other transformation types.

_getOriginalTransformsForLayerOrNull() (lines 154-182) logs an error when encountering unsupported transformations, yet always returns an IdentityTransform for that transformation. While this is acceptable error handling, consider either surfacing the error to the user in a user-friendly manner or preserving partial transformations that are valid when a single transform fails. This helps in debugging and user transparency.


195-230: Inversion logic for natively rendered layers.

In _getTransformsForLayerOrNull() (lines 195-230), the function applies the inverse of the nativelyRenderedLayerName’s transforms to the current layer. If the transformations remain consistent across layers, this should be fine. However, if future changes allow more complex or mismatched transformations, this logic might lead to unexpected results. Consider documenting the assumptions or adding checks.

CHANGELOG.unreleased.md (1)

14-14: Add missing comma after “dataset”.

In the phrase “possibility to configure a rotation for a dataset which can be toggled off and on…”, consider adding a comma after “dataset” to improve readability.

- Added the possibility to configure a rotation for a dataset which can be toggled off and on when viewing...
+ Added the possibility to configure a rotation for a dataset, which can be toggled off and on when viewing...
🧰 Tools
🪛 LanguageTool

[uncategorized] ~14-~14: Possible missing comma found.
Context: ...ssibility to configure a rotation for a dataset which can be toggled off and on when vi...

(AI_HYDRA_LEO_MISSING_COMMA)

frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (2)

57-63: Revisit repetitive structure in transformations array.

Here (lines 57-63), you repeatedly build the array of five transformations (translate → rotation x → rotation y → rotation z → translate). If this pattern is used across multiple places, consider extracting a utility function to avoid duplicated logic.


129-131: Explicit null check for onChange.

You are correctly checking (value != null) in the onChange handler so that zero is handled. However, a short inline comment explaining the rationale might help future maintainers understand why the check is != null instead of a simple truthiness check.

frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (4)

222-224: Consider memoizing the selector for better performance

The selector for hasLayerTransformsConfigured could benefit from memoization to prevent unnecessary recalculations.

Consider using createSelector from Redux Toolkit:

+import { createSelector } from '@reduxjs/toolkit';
+
+const selectHasLayerTransformsConfigured = createSelector(
+  [(state: OxalisState) => state.dataset, (_, layer) => layer],
+  (dataset, layer) => getTransformsForLayerOrNull(dataset, layer, null) != null
+);
+
 const hasLayerTransformsConfigured = useSelector(
-  (state: OxalisState) => getTransformsForLayerOrNull(state.dataset, layer, null) != null,
+  (state: OxalisState) => selectHasLayerTransformsConfigured(state, layer)
 );

242-245: Add accessibility attributes to disabled elements

The code correctly handles disabled state for layers without transforms, but could be more accessible.

Add ARIA attributes to improve accessibility:

   const isDisabled = isRenderedNatively && !hasLayerTransformsConfigured;
+  const ariaLabel = isDisabled
+    ? "Layer transformation controls (disabled - no transforms available)"
+    : "Layer transformation controls";

273-281: Consider using CSS-in-JS or styled-components

The inline styles could be moved to a styled component or CSS-in-JS solution for better maintainability.

Consider using styled-components:

+import styled from 'styled-components';
+
+const TransformationIconWrapper = styled.img<{ isDisabled: boolean }>`
+  width: 14px;
+  height: 14px;
+  margin-bottom: 4px;
+  margin-right: 5px;
+  cursor: ${props => props.isDisabled ? 'not-allowed' : 'pointer'};
+  opacity: ${props => props.isDisabled ? '0.5' : '1.0'};
+`;
+
-  const style = {
-    width: 14,
-    height: 14,
-    marginBottom: 4,
-    marginRight: 5,
-    ...(isDisabled
-      ? { cursor: "not-allowed", opacity: "0.5" }
-      : { cursor: "pointer", opacity: "1.0" }),
-  };

1218-1218: Consider type safety for skeleton layer transformation

The skeleton layer transformation could benefit from stronger typing.

Add type safety for the skeleton layer:

+type SkeletonLayer = {
+  category: 'skeleton';
+  name: string;
+};
+
-<TransformationIcon layer={{ category: "skeleton", name: tracingId }} />
+<TransformationIcon layer={{ category: "skeleton", name: tracingId } as SkeletonLayer} />
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 868585c and c22372d.

📒 Files selected for processing (20)
  • CHANGELOG.unreleased.md (1 hunks)
  • frontend/javascripts/admin/dataset/composition_wizard/04_configure_new_dataset.tsx (1 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (1 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (3 hunks)
  • frontend/javascripts/oxalis/api/api_latest.ts (1 hunks)
  • frontend/javascripts/oxalis/controller/scene_controller.ts (1 hunks)
  • frontend/javascripts/oxalis/geometries/materials/edge_shader.ts (1 hunks)
  • frontend/javascripts/oxalis/geometries/materials/node_shader.ts (1 hunks)
  • frontend/javascripts/oxalis/geometries/materials/plane_material_factory.ts (2 hunks)
  • frontend/javascripts/oxalis/merger_mode.ts (1 hunks)
  • frontend/javascripts/oxalis/model/accessors/dataset_layer_transformation_accessor.ts (1 hunks)
  • frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts (2 hunks)
  • frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts (3 hunks)
  • frontend/javascripts/oxalis/model/accessors/tool_accessor.ts (2 hunks)
  • frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.ts (1 hunks)
  • frontend/javascripts/oxalis/model/helpers/nml_helpers.ts (1 hunks)
  • frontend/javascripts/oxalis/model/sagas/dataset_saga.ts (1 hunks)
  • frontend/javascripts/oxalis/model/sagas/quick_select_heuristic_saga.ts (1 hunks)
  • frontend/javascripts/oxalis/model_initialization.ts (5 hunks)
  • frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (12)
  • frontend/javascripts/oxalis/model/helpers/nml_helpers.ts
  • frontend/javascripts/oxalis/controller/scene_controller.ts
  • frontend/javascripts/oxalis/merger_mode.ts
  • frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.ts
  • frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts
  • frontend/javascripts/oxalis/geometries/materials/node_shader.ts
  • frontend/javascripts/oxalis/geometries/materials/plane_material_factory.ts
  • frontend/javascripts/oxalis/model/sagas/dataset_saga.ts
  • frontend/javascripts/admin/dataset/composition_wizard/04_configure_new_dataset.tsx
  • frontend/javascripts/oxalis/api/api_latest.ts
  • frontend/javascripts/oxalis/geometries/materials/edge_shader.ts
  • frontend/javascripts/oxalis/model/accessors/tool_accessor.ts
👮 Files not reviewed due to content moderation or server errors (1)
  • frontend/javascripts/oxalis/model_initialization.ts
🧰 Additional context used
🪛 LanguageTool
CHANGELOG.unreleased.md

[uncategorized] ~14-~14: Possible missing comma found.
Context: ...ssibility to configure a rotation for a dataset which can be toggled off and on when vi...

(AI_HYDRA_LEO_MISSING_COMMA)

🔇 Additional comments (6)
frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts (2)

38-38: LGTM! Clean import of transformation functions.

The import of invertAndTranspose and getTransformsForLayer from the new dataset_layer_transformation_accessor module aligns with the PR's goal of centralizing transformation-related functions.


196-196: ⚠️ Potential issue

Critical fix: Ensure new matrix instance creation.

The change from M4x4.identity to M4x4.identity() is crucial as it ensures a new identity matrix instance is created rather than potentially sharing a reference. This prevents potential mutation-related bugs in matrix transformations.

Let's verify the usage of M4x4.identity() across the codebase to ensure consistency:

frontend/javascripts/oxalis/model/sagas/quick_select_heuristic_saga.ts (2)

61-61: LGTM: Import change aligns with transformation reorganization.

The relocation of getTransformsForLayer to a dedicated transformation accessor module improves code organization.


Line range hint 144-153: Verify transform comparison with rotated layers.

The transform comparison is crucial for the new rotation feature. While the implementation looks correct, we should verify that it handles all rotation scenarios correctly.

frontend/javascripts/oxalis/model/accessors/dataset_layer_transformation_accessor.ts (1)

130-140: Memoization map usage note.

memoizeWithThreeKeys() (lines 130-140) uses MultiKeyMap for caching. Consider verifying if the key parameters may be large or change frequently, impacting memory. If so, a cache invalidation policy or an upper-bound size check may help avoid unbounded growth.

frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (1)

203-225: Ensure consistent rotation defaults.

When setting initialDatasetRotationSettings (lines 203-225), you assume five transformations exist or use [0,0,0] by default. This is consistent with your “translate-rotate-rotate-rotate-translate” approach. If transformation definitions expand or a user manually modifies them, ensure that unexpected transformations do not silently get ignored.

✅ Verification successful

Let me gather more information about the transformation setup by checking the related code.


Let me check one more aspect to understand how the transformations are applied and validated.


Based on the gathered information, I can now provide the final response:

The five-transformation setup is a well-defined pattern in the codebase

The code's assumption of five transformations (translate-rotate-rotate-rotate-translate) is a deliberate design choice, consistently implemented across the codebase. The validation through doAllLayersHaveTheSameRotation ensures this pattern is maintained. When the pattern isn't present, the code safely defaults to [0,0,0] rotation, which is the correct fallback behavior. The transformation definitions are unlikely to silently get ignored as:

  • The database schema enforces the structure (dataset_layer_coordinateTransformations table)
  • The codebase consistently handles transformations through well-defined interfaces
  • The rotation extraction is specifically designed for 90-degree steps around main axes
  • Layer transformations are validated before being applied
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify if there are references to other transformation setups that might conflict.
rg 'coordinateTransformations'

Length of output: 14754


Script:

#!/bin/bash
# Check the implementation of getRotationFromTransformationIn90DegreeSteps and related functions
ast-grep --pattern 'function getRotationFromTransformationIn90DegreeSteps'

# Check for any other rotation-related transformations
rg -A 5 'rotation.*transform|transform.*rotation'

Length of output: 3910


Script:

#!/bin/bash
# Check for transformation validation or application logic
ast-grep --pattern 'function doAllLayersHaveTheSameRotation'

# Check for any other transformation validation
rg -A 5 'validateTransform|checkTransform|applyTransform'

Length of output: 5810

Comment on lines +396 to +427
function _doAllLayersHaveTheSameRotation(dataLayers: Array<APIDataLayer>): boolean {
const firstDataLayerTransformations = dataLayers[0]?.coordinateTransformations;
if (firstDataLayerTransformations == null || firstDataLayerTransformations.length === 0) {
// No transformations in all layers compatible with setting a rotation for the whole dataset.
return dataLayers.every(
(layer) =>
layer.coordinateTransformations == null || layer.coordinateTransformations.length === 0,
);
}
// There should be a translation to the origin, one transformation for each axis and one translation back. => A total of 5 affine transformations.
if (!hasValidTransformationCount(dataLayers) || !hasOnlyAffineTransformations(dataLayers)) {
return false;
}

if (!hasValidTransformationPattern(firstDataLayerTransformations)) {
return false;
}
for (let i = 1; i < dataLayers.length; i++) {
const transformations = dataLayers[i].coordinateTransformations;
if (
transformations == null ||
!_.isEqual(transformations[0], firstDataLayerTransformations[0]) ||
!_.isEqual(transformations[1], firstDataLayerTransformations[1]) ||
!_.isEqual(transformations[2], firstDataLayerTransformations[2]) ||
!_.isEqual(transformations[3], firstDataLayerTransformations[3]) ||
!_.isEqual(transformations[4], firstDataLayerTransformations[4])
) {
return false;
}
}
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Rotation consistency checks might be brittle.

_doAllLayersHaveTheSameRotation() (lines 396-427) strictly expects five transform steps (translate → rotate x → rotate y → rotate z → translate). This is clear for the current design, but might not scale well if transformations become more varied (e.g., scale, shear). Consider making the check more flexible or storing transform metadata if you foresee further extension of transformation configs.

Comment on lines +249 to +264
// Get getOriginalTransformsForLayerOrNull is not used to handle layers that do not support configuring a transformation.
const nextNativelyRenderedLayerName = isRenderedNatively ? null : layer.name;
const activeTransformation = getTransformsForLayer(
state.dataset,
layer,
state.datasetConfiguration.nativelyRenderedLayerName,
);
const invertedTransform = invertTransform(currentTransforms);
const newPosition = transformPointUnscaled(invertedTransform)(currentPosition);

// Also transform a reference coordinate to determine how the scaling
// changed. Then, adapt the zoom accordingly.
const referenceOffset: Vector3 = [10, 10, 10];
const secondPosition = V3.add(currentPosition, referenceOffset, [0, 0, 0]);
const newSecondPosition = transformPointUnscaled(invertedTransform)(secondPosition);

const scaleChange = _.mean(
// Only consider XY for now to determine the zoom change (by slicing from 0 to 2)
V3.abs(V3.divide3(V3.sub(newPosition, newSecondPosition), referenceOffset)).slice(0, 2),
const nextTransform = getTransformsForLayer(
state.dataset,
layer,
nextNativelyRenderedLayerName,
);
const { scaleChange, newPosition } = getNewPositionAndZoomChangeFromTransformationChange(
activeTransformation,
nextTransform,
state,
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

Improve error handling in transformation logic

The transformation logic should handle potential errors when retrieving transforms.

Add error handling and validation:

   const toggleLayerTransforms = () => {
     const state = Store.getState();
+    try {
       const nextNativelyRenderedLayerName = isRenderedNatively ? null : layer.name;
       const activeTransformation = getTransformsForLayer(
         state.dataset,
         layer,
         state.datasetConfiguration.nativelyRenderedLayerName,
       );
+      if (!activeTransformation) {
+        throw new Error('Failed to get active transformation');
+      }
       const nextTransform = getTransformsForLayer(
         state.dataset,
         layer,
         nextNativelyRenderedLayerName,
       );
+      if (!nextTransform) {
+        throw new Error('Failed to get next transformation');
+      }
+    } catch (error) {
+      console.error('Error toggling layer transforms:', error);
+      Toast.error('Failed to toggle layer transformation');
+      return;
+    }

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add "Rotate" button to dataset layers
2 participants