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

Merged
merged 46 commits into from
Jan 22, 2025

Conversation

MichaelBuessemeyer
Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer commented Oct 30, 2024

Related Slack canvas: https://scm.slack.com/docs/T02A8MN9K/F083DBRCFC6

--> 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.
frontend/javascripts/oxalis/model/accessors/dataset_accessor.ts Removed functions related to coordinate transformations.
Multiple files Relocated transformation-related imports from dataset_accessor to new transformation accessor module.

Assessment against linked issues

Objective Addressed Explanation
Rotate dataset layers (7334)
Persist rotation (7334)
Handle skeleton layer transformations (7334)

Possibly related PRs

  • Fix performance bottleneck when deleting a lot of trees at once #8176: The main PR modifies the import of flatToNestedMatrix from dataset_accessor to dataset_layer_transformation_accessor, which is also addressed in this PR that fixes a performance bottleneck related to tree deletions, indicating a shared focus on the dataset layer transformation functionality.
  • Misc layout improvements in dataset upload #8231: This PR includes fixes for performance issues when deleting multiple trees, which relates to the overall dataset management and transformation processes, similar to the changes made in the main PR regarding dataset handling.
  • Support OME-NGFF zarr dataset translation #8311: The main PR's changes to dataset transformations align with this PR's focus on supporting translations in OME-NGFF Zarr datasets, indicating a broader context of enhancing dataset transformation capabilities across multiple PRs.

Suggested labels

usability, bug, documentation

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 generate docstrings to generate docstrings for this PR. (Beta)
  • @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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (1)

48-71: ⚠️ Potential issue

Add cleanup to useEffect and handle race conditions.

The useEffect hook needs cleanup and race condition handling.

 useEffect(() => {
   if (
     datasetBoundingBox == null ||
     dataLayers[0].coordinateTransformations?.length !== EXPECTED_TRANSFORMATION_LENGTH ||
     !form
   ) {
     return;
   }
+  let isSubscribed = true;
   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,
   }));
+  if (isSubscribed) {
     form.setFieldValue(["dataSource", "dataLayers"], dataLayersWithUpdatedTransforms);
+  }
+  return () => {
+    isSubscribed = false;
+  };
 }, [datasetBoundingBox, dataLayers, form]);
🧹 Nitpick comments (2)
frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (2)

25-36: Consider using reduce for better readability and performance.

The function could be refactored to use reduce for a more functional and potentially more performant approach.

 function getDatasetBoundingBoxFromLayers(layers: APIDataLayer[]): BoundingBox | undefined {
   if (!layers || layers.length === 0) {
     return undefined;
   }
-  let datasetBoundingBox = BoundingBox.fromBoundBoxObject(layers[0].boundingBox);
-  for (let i = 1; i < layers.length; i++) {
-    datasetBoundingBox = datasetBoundingBox.extend(
-      BoundingBox.fromBoundBoxObject(layers[i].boundingBox),
-    );
-  }
-  return datasetBoundingBox;
+  return layers.reduce((acc, layer) => {
+    const layerBox = BoundingBox.fromBoundBoxObject(layer.boundingBox);
+    return acc ? acc.extend(layerBox) : layerBox;
+  }, undefined as BoundingBox | undefined);
 }

157-182: Extract tooltip content and improve error message.

The tooltip content could be extracted for reusability and the error message could be more concise.

+const ROTATION_REQUIREMENTS = {
+  TITLE: "Setting a dataset's rotation requires consistent layer transformations",
+  STEPS: [
+    "Translation to the origin",
+    "Rotation around the x-axis",
+    "Rotation around the y-axis",
+    "Rotation around the z-axis",
+    "Translation back to the original position",
+  ],
+  RESOLUTION: "To enable this setting, remove all coordinateTransformations from all layers in the advanced tab, save and reload the dataset settings.",
+} as const;

 if (!isRotationOnly) {
   return (
     <Tooltip
       title={
         <div>
-          Each layers transformations must be equal and each layer needs exactly 5 affine
-          transformation with the following schema:
+          Each layer must have exactly 5 identical affine transformations in this order:
           <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>
+            {ROTATION_REQUIREMENTS.STEPS.map((step, index) => (
+              <li key={index}>{step}</li>
+            ))}
           </ul>
-          To easily enable this setting, delete all coordinateTransformations of all layers in the
-          advanced tab, save and reload the dataset settings.
+          {ROTATION_REQUIREMENTS.RESOLUTION}
         </div>
       }
     >
       <Text type="secondary">
-        Setting a dataset's rotation is only supported when all layers have the same rotation
-        transformation. <InfoCircleOutlined />
+        {ROTATION_REQUIREMENTS.TITLE} <InfoCircleOutlined />
       </Text>
     </Tooltip>
   );
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between bae0048 and 0cb6992.

📒 Files selected for processing (1)
  • frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: circleci_build
🔇 Additional comments (1)
frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx (1)

1-24: LGTM! Well-organized imports and type definitions.

The imports are properly organized, and the type definitions are clear and well-structured.

@philippotto
Copy link
Member

From slack: https://scm.slack.com/archives/C5AKLAV0B/p1736434462796459

i just tested the rotation feature a bit and i noticed that e.g. the hovering of segment ids does not work if the segmentation layer is rotated. this was probably already broken independently of your PR (but i didn't notice it because the previous feature was not used in this context). you would probably have to consider the rotation matrix when looking up from mouse position to voxel position

Should be fixed now with commit 3f1bdb9

Hmm, still doesn't work for me? I tested it here: https://allowpermadatasetrotation.webknossos.xyz/datasets/l4_sample-677e47bc010000b800249ffc/view#3584,3584,1024,0,1.3

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
frontend/javascripts/oxalis/model.ts (2)

229-233: Consider caching the state and layer lookup.

The state retrieval and layer lookup are performed for every mouse position update, which could impact performance during rapid mouse movements. Consider caching these values and updating them only when the state changes.

+ // Cache these values and update them only when state changes
  const state = Store.getState();
  const additionalCoordinates = state.flycam.additionalCoordinates;
  const segmentationDataLayer = state.dataset.dataSource.dataLayers.find(
    (dataLayer) => dataLayer.name === segmentationLayer.name,
  );

234-243: Add error handling and type safety improvements.

The transformation logic could benefit from:

  1. Error logging for transformation failures
  2. Explicit type annotation for the transformed position
  if (segmentationDataLayer) {
    const segmentationLayerTransforms = getTransformsForLayerOrNull(
      state.dataset,
      segmentationDataLayer,
      state.datasetConfiguration.nativelyRenderedLayerName,
    );
    if (segmentationLayerTransforms) {
-     pos = transformPointUnscaled(invertTransform(segmentationLayerTransforms))(pos);
+     try {
+       const transformedPos: Vector3 = transformPointUnscaled(
+         invertTransform(segmentationLayerTransforms)
+       )(pos);
+       pos = transformedPos;
+     } catch (error) {
+       console.error('Failed to transform position:', error);
+       // Continue with untransformed position
+     }
    }
  }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0cb6992 and 3c06db5.

📒 Files selected for processing (2)
  • CHANGELOG.unreleased.md (1 hunks)
  • frontend/javascripts/oxalis/model.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.unreleased.md
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: circleci_build
🔇 Additional comments (1)
frontend/javascripts/oxalis/model.ts (1)

24-25: LGTM! Clear and well-organized imports.

The new transformation-related imports are appropriately placed and follow the project's module structure.

@MichaelBuessemeyer
Copy link
Contributor Author

Hmm, still doesn't work for me? I tested it here: https://allowpermadatasetrotation.webknossos.xyz/datasets/l4_sample-677e47bc010000b800249ffc/view#3584,3584,1024,0,1.3

Oh well. Thanks for finding this. I still had a little logic bug (I applied the segmentation layer's transform instead of the inverse of the transform). The newest commit 3c06db5 should fix this. ™️

Testing for me worked because I only had a 180 degree rotation for x configured and in that case applying the same rotation again instead of the inverse seems to result in the correct behaviour. As your test case was more complex and it worked for me locally with these settings, this should hopefully be fixed now :)

@philippotto
Copy link
Member

Great! Unfortunately, the context menu uses the wrong position apparently (e.g., load mesh or the segment id in the context menu footer is wrong). Can you have another look? 🙈

@MichaelBuessemeyer
Copy link
Contributor Author

While working on this I also noticed that bounding boxes are not rotated 🙈? Should this be included here as well or in a follow up pr?

@MichaelBuessemeyer
Copy link
Contributor Author

Same for meshes 🙈

@philippotto
Copy link
Member

Bummer! The layer bboxes are able to transform according to the active layer transform. user bounding boxes probably need to use the same transforms as the skeleton layer. meshes should use the same transforms as their segmentation layer.
however, I would suggest to do this later as a follow-up pr, as you have other stuff on our plate, too. can you open an issue? :)

@MichaelBuessemeyer
Copy link
Contributor Author

MichaelBuessemeyer commented Jan 20, 2025

The issue is here: #8340

And I found a but that toggling the skeleton layer crashed wk. I fixed this by disabling toggling skeleton layer / letting the user configure them as the active native layer. The mini discussion about this can be found here: https://scm.slack.com/archives/C083DBRCFC6/p1737378159151899

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

🔭 Outside diff range comments (2)
frontend/javascripts/oxalis/view/context_menu.tsx (2)

Line range hint 1447-1467: Improve error handling in getContextMenuPositionFromEvent.

The function returns [0, 0] when no valid reference div is found, which could lead to incorrect menu positioning and a confusing user experience. Consider throwing an error or logging a warning in this case.

-  if (referenceDiv == null) {
-    return [0, 0];
-  }
+  if (referenceDiv == null) {
+    console.warn('No valid reference div found for context menu positioning');
+    // Return a position that ensures the menu is visible within the viewport
+    return [event.clientX, event.clientY];
+  }

Line range hint 186-205: Add input sanitization for clipboard operations.

The function writes values directly to clipboard without sanitization, which could be a security risk if the values contain malicious code.

+function sanitizeClipboardText(value: string | number): string {
+  // Convert to string and remove any potentially harmful characters
+  return String(value).replace(/[<>]/g, '');
+}

function copyIconWithTooltip(value: string | number, title: string) {
  return (
    <FastTooltip title={title}>
      <CopyOutlined
        style={{
          margin: "0 0 0 5px",
        }}
        onClick={async () => {
-         await navigator.clipboard.writeText(value.toString());
+         const sanitizedValue = sanitizeClipboardText(value);
+         await navigator.clipboard.writeText(sanitizedValue);
          Toast.success(`"${value}" copied to clipboard`);
        }}
      />
    </FastTooltip>
  );
}
🧹 Nitpick comments (10)
frontend/javascripts/oxalis/view/context_menu.tsx (1)

Line range hint 1284-1288: Optimize performance by memoizing the refresh handler.

The handleRefreshSegmentVolume function is recreated on every render. Consider using useCallback to memoize it.

-  const handleRefreshSegmentVolume = async () => {
-    await api.tracing.save();
-    setLastTimeSegmentInfoShouldBeFetched(new Date());
-  };
+  const handleRefreshSegmentVolume = useCallback(async () => {
+    await api.tracing.save();
+    setLastTimeSegmentInfoShouldBeFetched(new Date());
+  }, []);
frontend/javascripts/oxalis/controller/combinations/volume_handlers.ts (2)

86-91: Consider caching transformed positions for async operations.

While the position transformation is correctly implemented, consider caching the transformed position if it's used frequently in async operations to avoid redundant calculations.

+ // Cache the transformed position if used frequently
  const posInLayerSpace = memoizeOne(
    (globalPos: Vector3, layerName: string) =>
      globalToLayerTransformedPosition(
        globalPos,
        layerName,
        "segmentation",
        Store.getState(),
      ),
    ([pos1, name1], [pos2, name2]) =>
      V3.isEqual(pos1, pos2) && name1 === name2,
  );

Also applies to: 97-97, 102-102, 107-107


Line range hint 55-107: Architecture feedback: Position transformation implementation aligns with rotation feature.

The implementation of position transformation for rotated layers is well-structured and maintains consistency across both synchronous and asynchronous operations. The approach:

  1. Cleanly separates transformation logic into a dedicated accessor
  2. Consistently applies transformations before data access
  3. Preserves existing functionality while adding rotation support

This aligns well with the PR's objective of supporting dataset layer rotation.

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

34-44: Add JSDoc comments for constants.

Consider adding JSDoc comments to document the purpose and structure of IDENTITY_MATRIX and IDENTITY_TRANSFORM constants. This would improve code maintainability and help other developers understand their usage.

+/**
+ * 4x4 identity matrix in nested format.
+ * Used as a default transformation matrix.
+ */
 const IDENTITY_MATRIX = [
   [1, 0, 0, 0],
   [0, 1, 0, 0],
   [0, 0, 1, 0],
   [0, 0, 0, 1],
 ] as NestedMatrix4;

+/**
+ * Default coordinate transformation using identity matrix.
+ * Represents no transformation (identity transformation).
+ */
 export const IDENTITY_TRANSFORM: CoordinateTransformation = {
   type: "affine",
   matrix: IDENTITY_MATRIX,
 };

115-127: Optimize matrix rotation performance.

The current implementation creates new THREE.js objects for each rotation. Consider caching these objects to improve performance, especially when called frequently.

+// Cache THREE.js objects for better performance
+const cachedEuler = new THREE.Euler();
+const cachedRotationMatrix = new THREE.Matrix4();
+
 export function getRotationMatrixAroundAxis(
   axis: "x" | "y" | "z",
   angleInRadians: number,
 ): AffineTransformation {
-  const euler = new THREE.Euler();
-  euler[axis] = angleInRadians;
-  const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(euler).transpose(); // Column-major to row-major
+  cachedEuler[axis] = angleInRadians;
+  cachedRotationMatrix.makeRotationFromEuler(cachedEuler).transpose(); // Column-major to row-major
   const matrixWithoutNearlyZeroValues = rotationMatrix
     .toArray()
     // Avoid nearly zero values due to floating point arithmetic inaccuracies.
     .map((value) => (Math.abs(value) < Number.EPSILON ? 0 : value)) as Matrix4x4;
   return { type: "affine", matrix: flatToNestedMatrix(matrixWithoutNearlyZeroValues) };
 }

404-435: Optimize rotation validation performance.

The _doAllLayersHaveTheSameRotation function performs multiple deep comparisons and matrix operations, which could be optimized:

  1. Cache intermediate results
  2. Early exit on validation failures
  3. Reduce redundant checks
 function _doAllLayersHaveTheSameRotation(dataLayers: Array<APIDataLayer>): boolean {
+  if (dataLayers.length <= 1) {
+    return true;
+  }
+
   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,
     );
   }
+
+  // Early validation to avoid unnecessary processing
+  if (!hasValidTransformationCount(dataLayers) || !hasOnlyAffineTransformations(dataLayers)) {
+    return false;
+  }
+
+  if (!hasValidTransformationPattern(firstDataLayerTransformations)) {
+    return false;
+  }
+
+  // Cache the first layer's transformations for comparison
+  const referenceTransforms = firstDataLayerTransformations.map(t => JSON.stringify(t));
+
   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])
-    ) {
+    if (!transformations) {
       return false;
     }
+    // Compare stringified versions to avoid deep comparisons
+    for (let j = 0; j < EXPECTED_TRANSFORMATION_LENGTH; j++) {
+      if (JSON.stringify(transformations[j]) !== referenceTransforms[j]) {
+        return false;
+      }
+    }
   }
   return true;
 }
frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (4)

228-229: Consider improving the comment clarity.

The comment could be more concise and clearer.

-    // Set nativelyRenderedLayerName to null in case the current layer is already natively rendered or does not have its own transformations configured (e.g. a skeleton layer) .
+    // Reset nativelyRenderedLayerName if layer is natively rendered or lacks custom transforms (e.g., skeleton layer)

230-244: Consider extracting transformation logic to a separate function.

The transformation toggle logic is complex and would benefit from being extracted into a dedicated function for better maintainability.

+  const calculateTransformationChange = (
+    state: OxalisState,
+    layer: APIDataLayer | APISkeletonLayer,
+    nextNativelyRenderedLayerName: string | null
+  ) => {
+    const activeTransformation = getTransformsForLayer(
+      state.dataset,
+      layer,
+      state.datasetConfiguration.nativelyRenderedLayerName,
+    );
+    const nextTransform = getTransformsForLayer(
+      state.dataset,
+      layer,
+      nextNativelyRenderedLayerName,
+    );
+    return getNewPositionAndZoomChangeFromTransformationChange(
+      activeTransformation,
+      nextTransform,
+      state,
+    );
+  };

   const toggleLayerTransforms = () => {
     const state = Store.getState();
     const nextNativelyRenderedLayerName = isRenderedNatively ? null : layer.name;
-    const activeTransformation = getTransformsForLayer(
-      state.dataset,
-      layer,
-      state.datasetConfiguration.nativelyRenderedLayerName,
-    );
-    const nextTransform = getTransformsForLayer(
-      state.dataset,
-      layer,
-      nextNativelyRenderedLayerName,
-    );
-    const { scaleChange, newPosition } = getNewPositionAndZoomChangeFromTransformationChange(
-      activeTransformation,
-      nextTransform,
-      state,
-    );
+    const { scaleChange, newPosition } = calculateTransformationChange(
+      state,
+      layer,
+      nextNativelyRenderedLayerName
+    );

252-260: Consider moving styles to CSS.

The inline styles should be moved to a CSS file for better maintainability and reusability.

+// Add to CSS file:
+.transformation-icon {
+  width: 14px;
+  height: 14px;
+  margin-bottom: 4px;
+  margin-right: 5px;
+}
+
+.transformation-icon--disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+
+.transformation-icon--enabled {
+  cursor: pointer;
+  opacity: 1.0;
+}

-  const style = {
-    width: 14,
-    height: 14,
-    marginBottom: 4,
-    marginRight: 5,
-    ...(isDisabled
-      ? { cursor: "not-allowed", opacity: "0.5" }
-      : { cursor: "pointer", opacity: "1.0" }),
-  };
+  const className = `transformation-icon ${
+    isDisabled ? 'transformation-icon--disabled' : 'transformation-icon--enabled'
+  }`;

266-270: Simplify the tooltip text construction.

The template literal could be simplified for better readability.

-          isRenderedNatively
-            ? `This layer is shown natively (i.e., without any transformations).${isDisabled ? "" : " Click to render this layer with its configured transforms."}`
-            : `This layer is rendered with ${
-                typeToLabel[transform.type]
-              } transformation.${isDisabled ? "" : " Click to render this layer without any transforms."}`
+          const baseText = isRenderedNatively
+            ? "This layer is shown natively (i.e., without any transformations)"
+            : `This layer is rendered with ${typeToLabel[transform.type]} transformation`;
+          const actionText = isDisabled
+            ? ""
+            : isRenderedNatively
+            ? " Click to render this layer with its configured transforms"
+            : " Click to render this layer without any transforms";
+          `${baseText}.${actionText}`
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3c06db5 and 1d00a5d.

📒 Files selected for processing (10)
  • CHANGELOG.unreleased.md (1 hunks)
  • frontend/javascripts/oxalis/api/api_latest.ts (1 hunks)
  • frontend/javascripts/oxalis/controller/combinations/volume_handlers.ts (3 hunks)
  • frontend/javascripts/oxalis/model.ts (2 hunks)
  • frontend/javascripts/oxalis/model/accessors/dataset_layer_transformation_accessor.ts (1 hunks)
  • frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts (3 hunks)
  • frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts (2 hunks)
  • frontend/javascripts/oxalis/model/helpers/nml_helpers.ts (1 hunks)
  • frontend/javascripts/oxalis/view/context_menu.tsx (1 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 (6)
  • frontend/javascripts/oxalis/model/helpers/nml_helpers.ts
  • frontend/javascripts/oxalis/model.ts
  • frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.ts
  • frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts
  • frontend/javascripts/oxalis/api/api_latest.ts
  • CHANGELOG.unreleased.md
🔇 Additional comments (9)
frontend/javascripts/oxalis/view/context_menu.tsx (1)

1050-1050: LGTM: Proper handling of transformed layers.

The code correctly disables node creation when the skeleton layer is transformed, which aligns with the PR objectives for dataset layer rotation functionality.

frontend/javascripts/oxalis/controller/combinations/volume_handlers.ts (3)

5-5: LGTM! Import statement aligns with layer transformation requirements.

The addition of globalToLayerTransformedPosition import supports the new dataset layer rotation feature.


66-66: LGTM! Consistent usage of transformed positions.

The code consistently uses the transformed position (posInLayerSpace) across all operations, maintaining correctness when working with rotated layers.

Also applies to: 69-69


55-60: Verify position transformation in segmentation operations.

The transformation of global positions to layer space is crucial for correct segment ID lookup when layers are rotated. However, we should verify that this transformation is consistently applied across all segmentation operations.

✅ Verification successful

Position transformation is consistently applied in segmentation operations

The transformation of global positions to layer space is correctly and consistently implemented across all segmentation operations in the codebase. The reviewed changes align with the existing pattern where globalToLayerTransformedPosition is called before accessing segmentation data.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other segmentation-related operations that might need position transformation
rg -l 'segmentationCube\.getMappedDataValue|getRenderedZoomStepAtPosition' --type ts

# Search for direct usage of global positions that might need transformation
ast-grep --pattern 'globalPos' --lang ts

Length of output: 2781


Script:

#!/bin/bash
# Check segmentation operations in api_latest.ts
rg -A 5 'segmentationCube\.getMappedDataValue|getRenderedZoomStepAtPosition' frontend/javascripts/oxalis/api/api_latest.ts

# Check implementation of segmentation operations in volume_handlers.ts
rg -A 5 'segmentationCube\.getMappedDataValue|getRenderedZoomStepAtPosition' frontend/javascripts/oxalis/controller/combinations/volume_handlers.ts

# Check for layer transformation usage
rg -A 3 'globalToLayerTransformedPosition' --type ts

Length of output: 3613

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

74-98: 🛠️ Refactor suggestion

Enhance rotation handling precision and error handling.

The rotation calculation has several areas for improvement:

  1. Magic number 1e-6 should be defined as a constant
  2. Edge cases in angle calculation need better handling
  3. Function lacks input validation

Apply this diff to improve the implementation:

+const ROTATION_EPSILON = 1e-6;
+
 export function getRotationFromTransformationIn90DegreeSteps(
   transformation: CoordinateTransformation | undefined,
   axis: "x" | "y" | "z",
 ) {
+  if (axis == null) {
+    throw new Error("Axis must be specified");
+  }
+
   if (transformation && transformation.type !== "affine") {
     return 0;
   }
   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]];
+  
+  // Handle edge cases where both sine and cosine are close to zero
+  if (Math.abs(sinOfAngle) < ROTATION_EPSILON && Math.abs(cosOfAngle) < ROTATION_EPSILON) {
+    return 0;
+  }
+
   const rotation =
-    Math.abs(cosOfAngle) > 1e-6 // Avoid division by zero
+    Math.abs(cosOfAngle) > ROTATION_EPSILON
       ? Math.atan2(sinOfAngle, cosOfAngle)
       : sinOfAngle > 0
         ? Math.PI / 2
         : -Math.PI / 2;

Likely invalid or redundant comment.

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

55-68: LGTM! Well-organized imports for transformation support.

The reorganization of imports improves code organization by:

  • Centralizing transformation-related functions in a dedicated accessor
  • Adding necessary flycam-related functions for position/zoom handling

198-201: LGTM! Clear layer transformation capability checks.

The checks for layer transformation support are well-implemented and clearly separated.


1191-1191: Verify skeleton layer transformation behavior.

Please ensure that skeleton layer transformations work correctly with the new transformation system, particularly when toggling between native and transformed rendering.

Also applies to: 1241-1241

✅ Verification successful

Skeleton layer transformation implementation verified.

The codebase shows proper integration between skeleton layer visibility controls and the transformation system, with dedicated state management, transformation accessors, and shader support for both native and transformed rendering.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for potential issues with skeleton layer transformations
ast-grep --pattern 'getTransformsForLayer($_, { category: "skeleton" }, $_)'

Length of output: 78


Script:

#!/bin/bash
# Search for skeleton-related transformation code
echo "=== Searching for skeleton transformation implementations ==="
ast-grep --pattern 'transform($_) { $$$'

echo "=== Checking for skeleton-specific layer handling ==="
rg -l "skeleton.*transform|transform.*skeleton" --type typescript

echo "=== Looking for transformation toggle logic ==="
rg -l "toggleTransform|setTransform.*skeleton" --type typescript

Length of output: 602


Script:

#!/bin/bash
# Search for transformation-related code in TS files
echo "=== Searching for transformation implementations ==="
fd -e ts -e tsx -x rg -l "transform.*skeleton|skeleton.*transform"

echo "=== Checking layer settings implementations ==="
fd -e ts -e tsx -x rg -l "showSkeletons.*transform|transform.*layer"

echo "=== Looking for toggle/state management code ==="
fd "layer_settings_tab.tsx" -x rg -A 5 "showSkeletons"

Length of output: 2648


314-315: Verify bounding box calculations with transformations.

Ensure that bounding box calculations correctly handle transformed layers.

✅ Verification successful

Bounding box calculations correctly handle transformations

The implementation properly handles transformed layers by:

  • Applying appropriate matrix transformations to coordinates
  • Providing clear UI indicators for transformation state
  • Supporting both affine and thin-plate-spline transformations
  • Maintaining correct coordinate systems for display
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check bounding box handling with transformations
ast-grep --pattern 'layer.boundingBox.topLeft'

Length of output: 912


Script:

#!/bin/bash
# Search for layer transformation and bounding box related code
echo "=== Searching for layer transformation code ==="
rg -l "transform.*layer|layer.*transform" --type typescript

echo -e "\n=== Searching for bounding box calculations ==="
ast-grep --pattern 'boundingBox = '

echo -e "\n=== Looking for matrix transformation utilities ==="
rg -l "matrix.*transform|transform.*matrix" --type typescript

Length of output: 66111


Script:

#!/bin/bash
# Search for bounding box and transformation related code in the specific file
echo "=== Checking layer_settings_tab.tsx ==="
cat frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx

echo -e "\n=== Searching for transformation related code without file type restrictions ==="
rg "transform.*layer|layer.*transform" frontend/javascripts/oxalis/view/

echo -e "\n=== Searching for bounding box calculations ==="
rg "boundingBox" frontend/javascripts/oxalis/view/

Length of output: 68937

@MichaelBuessemeyer
Copy link
Contributor Author

Moreover, the newest commits should hopefully fix all code locations that try to get the segmentation information based on a global position.

Please give this another PR testing try and check my newest changes 🙏

I am sorry, that this PR takes so many iterations.

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.

Great! I found some other stuff but commented that in the follow-up issue, because it's not caused by this PR. let's merge and iterate from there :)

@MichaelBuessemeyer MichaelBuessemeyer enabled auto-merge (squash) January 22, 2025 12:28
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

220-225: Consider adding a comment explaining the complex condition.

The disabled state logic is complex and would benefit from a more detailed explanation of the business rules.

-  // Cannot toggle transforms for a layer that cannot have no transforms or turn them on in case the layer has no transforms.
-  // Layers that cannot have transformations like skeleton layer and volume tracing layers without fallback
-  // automatically copy to the dataset transformation if all other layers have the same transformation.
+  // The transformation toggle is disabled in two cases:
+  // 1. The layer cannot have transformations (e.g., skeleton layers, volume tracing layers without fallback)
+  // 2. The layer is currently rendered natively (no transformation) and has no transforms configured
+  // Note: Layers without transformation support automatically inherit the dataset transformation
+  // when all other layers have the same transformation.

252-260: Consider extracting style object to constants.

The style object could be moved to a constants section at the top of the file for better maintainability.

+const TRANSFORMATION_ICON_STYLE = {
+  width: 14,
+  height: 14,
+  marginBottom: 4,
+  marginRight: 5,
+};
+
+const getTransformationIconStyle = (isDisabled: boolean) => ({
+  ...TRANSFORMATION_ICON_STYLE,
+  ...(isDisabled
+    ? { cursor: "not-allowed", opacity: "0.5" }
+    : { cursor: "pointer", opacity: "1.0" }),
+});

   const style = getTransformationIconStyle(isDisabled);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1d00a5d and 472c1a8.

📒 Files selected for processing (2)
  • CHANGELOG.unreleased.md (1 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 (1)
  • CHANGELOG.unreleased.md
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: circleci_build
🔇 Additional comments (5)
frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx (5)

55-68: LGTM! Clean reorganization of imports.

The imports have been properly reorganized to support the new transformation functionality, with clear separation of concerns between dataset, layer transformation, and flycam accessors.


198-202: LGTM! Clear and well-structured layer transformation checks.

The hooks are properly used to determine transformation capabilities and configuration state.


771-772: LGTM! Icon spacing fixed as noted in previous review.

The icon margin has been properly adjusted to maintain consistent spacing.


1191-1191: LGTM! Proper integration of transformation support for skeleton layers.

The skeleton layer now correctly supports transformations while maintaining its existing functionality.

Also applies to: 1241-1241


228-244: 🛠️ Refactor suggestion

Add error handling for transformation changes.

The transformation toggle logic should handle potential errors when retrieving or applying transforms.

   const toggleLayerTransforms = () => {
     const state = Store.getState();
+    try {
       // Set nativelyRenderedLayerName to null in case the current layer is already natively rendered or does not have its own transformations configured (e.g. a skeleton layer) .
       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');
+      }
       const { scaleChange, newPosition } = getNewPositionAndZoomChangeFromTransformationChange(
         activeTransformation,
         nextTransform,
         state,
       );
       dispatch(
         updateDatasetSettingAction("nativelyRenderedLayerName", nextNativelyRenderedLayerName),
       );
       dispatch(setPositionAction(newPosition));
       dispatch(setZoomStepAction(state.flycam.zoomStep * scaleChange));
+    } catch (error) {
+      console.error('Error toggling layer transforms:', error);
+      Toast.error('Failed to toggle layer transformation');
+    }
   };

Likely invalid or redundant comment.

@MichaelBuessemeyer MichaelBuessemeyer merged commit 1fed6d3 into master Jan 22, 2025
3 checks passed
@MichaelBuessemeyer MichaelBuessemeyer deleted the allow-perma-dataset-rotation branch January 22, 2025 12:42
@MichaelBuessemeyer MichaelBuessemeyer restored the allow-perma-dataset-rotation branch January 24, 2025 19:42
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