From 0d4925ced9e4ee425029fd7709390b280fafe28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 6 May 2025 15:36:05 +0200 Subject: [PATCH 001/128] wip: enable rotation for ortho views --- .../oxalis/controller/scene_controller.ts | 15 ++++++++--- .../javascripts/oxalis/geometries/plane.ts | 26 ++++++++++++++----- .../oxalis/model/reducers/flycam_reducer.ts | 6 ----- .../oxalis/model/reducers/settings_reducer.ts | 8 +----- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index c52a1697bce..80f9bb2a7ef 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -69,10 +69,19 @@ THREE.Mesh.prototype.raycast = acceleratedRaycast; const CUBE_COLOR = 0x999999; const LAYER_CUBE_COLOR = 0xffff99; +export const OrthoBaseRotations = { + [OrthoViews.PLANE_XY]: new THREE.Euler(0, Math.PI, 0), + [OrthoViews.PLANE_YZ]: new THREE.Euler(Math.PI, (1 / 2) * Math.PI, 0), + [OrthoViews.PLANE_XZ]: new THREE.Euler((-1 / 2) * Math.PI, 0, 0), + [OrthoViews.TDView]: new THREE.Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4), +}; + + const getVisibleSegmentationLayerNames = reuseInstanceOnEquality((storeState: WebknossosState) => getVisibleSegmentationLayers(storeState).map((l) => l.name), ); + class SceneController { skeletons: Record = {}; isPlaneVisible: OrthoViewMap; @@ -242,9 +251,9 @@ class SceneController { [OrthoViews.PLANE_YZ]: new Plane(OrthoViews.PLANE_YZ), [OrthoViews.PLANE_XZ]: new Plane(OrthoViews.PLANE_XZ), }; - this.planes[OrthoViews.PLANE_XY].setRotation(new THREE.Euler(Math.PI, 0, 0)); - this.planes[OrthoViews.PLANE_YZ].setRotation(new THREE.Euler(Math.PI, (1 / 2) * Math.PI, 0)); - this.planes[OrthoViews.PLANE_XZ].setRotation(new THREE.Euler((-1 / 2) * Math.PI, 0, 0)); + this.planes[OrthoViews.PLANE_XY].setBaseRotation(OrthoBaseRotations[OrthoViews.PLANE_XY]); + this.planes[OrthoViews.PLANE_YZ].setBaseRotation(OrthoBaseRotations[OrthoViews.PLANE_YZ]); + this.planes[OrthoViews.PLANE_XZ].setBaseRotation(OrthoBaseRotations[OrthoViews.PLANE_XZ]); const planeMeshes = _.values(this.planes).flatMap((plane) => plane.getMeshes()); this.rootNode = new THREE.Group().add( diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index 1e064a2fbfc..63812c4c919 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -31,7 +31,7 @@ class Plane { // This class is supposed to collect all the Geometries that belong to one single plane such as // the plane itself, its texture, borders and crosshairs. // @ts-expect-error ts-migrate(2564) FIXME: Property 'plane' has no initializer and is not def... Remove this comment to see the full error message - plane: THREE.Mesh; + plane: THREE.Mesh; planeID: OrthoView; materialFactory!: PlaneMaterialFactory; displayCrosshair: boolean; @@ -41,6 +41,7 @@ class Plane { // @ts-expect-error ts-migrate(2564) FIXME: Property 'TDViewBorders' has no initializer and is... Remove this comment to see the full error message TDViewBorders: THREE.Line; lastScaleFactors: [number, number]; + baseRotation: THREE.Euler; constructor(planeID: OrthoView) { this.planeID = planeID; @@ -53,6 +54,7 @@ class Plane { const baseVoxelFactors = getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale); const scaleArray = Dimensions.transDim(baseVoxelFactors, this.planeID); this.baseScaleVector = new THREE.Vector3(...scaleArray); + this.baseRotation = new THREE.Euler(0, 0, 0); this.createMeshes(); } @@ -62,11 +64,13 @@ class Plane { const planeGeo = new THREE.PlaneGeometry(pWidth, pWidth, PLANE_SUBDIVISION, PLANE_SUBDIVISION); this.materialFactory = new PlaneMaterialFactory( this.planeID, - true, + false, OrthoViewValues.indexOf(this.planeID), ); const textureMaterial = this.materialFactory.setup().getMaterial(); this.plane = new THREE.Mesh(planeGeo, textureMaterial); + this.plane.name = `${this.planeID}-plane`; + this.plane.material.side = THREE.DoubleSide; // create crosshair const crosshairGeometries = []; this.crosshair = new Array(2); @@ -94,6 +98,8 @@ class Plane { // The default renderOrder is 0. In order for the crosshairs to be shown // render them AFTER the plane has been rendered. this.crosshair[i].renderOrder = 1; + this.crosshair[i].name = `${this.planeID}-crosshair-${i}`; + this.crosshair[i].matrixAutoUpdate = false; } // create borders @@ -109,6 +115,8 @@ class Plane { tdViewBordersGeo, this.getLineBasicMaterial(OrthoViewColors[this.planeID], 1), ); + this.TDViewBorders.name = `${this.planeID}-TDViewBorders`; + this.TDViewBorders.matrixAutoUpdate = false; } setDisplayCrosshair = (value: boolean): void => { @@ -156,9 +164,16 @@ class Plane { this.crosshair[1].scale.copy(scaleVec); } + setBaseRotation = (rotVec: THREE.Euler): void => { + this.baseRotation.copy(rotVec); + }; + setRotation = (rotVec: THREE.Euler): void => { - [this.plane, this.TDViewBorders, this.crosshair[0], this.crosshair[1]].map((mesh) => - mesh.setRotationFromEuler(rotVec), + const baseRotationMatrix = new THREE.Matrix4().makeRotationFromEuler(this.baseRotation); + const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(rotVec); + const combinedMatrix = baseRotationMatrix.multiply(rotationMatrix); + this.getMeshes().map((mesh) => + mesh.setRotationFromMatrix(combinedMatrix), ); }; @@ -174,10 +189,8 @@ class Plane { this.plane.position.set(x, y, z); if (originalPosition == null) { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'setGlobalPosition' does not exist on typ... Remove this comment to see the full error message this.plane.material.setGlobalPosition(x, y, z); } else { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'setGlobalPosition' does not exist on typ... Remove this comment to see the full error message this.plane.material.setGlobalPosition( originalPosition[0], originalPosition[1], @@ -195,7 +208,6 @@ class Plane { getMeshes = () => [this.plane, this.TDViewBorders, this.crosshair[0], this.crosshair[1]]; setLinearInterpolationEnabled = (enabled: boolean) => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'setUseBilinearFiltering' does not exist ... Remove this comment to see the full error message this.plane.material.setUseBilinearFiltering(enabled); }; diff --git a/frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts b/frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts index 0d57ea79bc3..96048cc01f1 100644 --- a/frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts @@ -275,13 +275,7 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState } case "SET_ROTATION": { - // This action should only be dispatched when *not* being in orthogonal mode, - // because this would lead to incorrect buckets being selected for rendering. - if (state.temporaryConfiguration.viewMode !== "orthogonal") { return setRotationReducer(state, action.rotation); - } - // No-op - return state; } case "SET_DIRECTION": { diff --git a/frontend/javascripts/oxalis/model/reducers/settings_reducer.ts b/frontend/javascripts/oxalis/model/reducers/settings_reducer.ts index 6ec93ea408a..3759b17ea1a 100644 --- a/frontend/javascripts/oxalis/model/reducers/settings_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/settings_reducer.ts @@ -182,13 +182,7 @@ function SettingsReducer(state: WebknossosState, action: Action): WebknossosStat const newState = updateTemporaryConfig(state, { viewMode: action.viewMode, }); - if (action.viewMode !== "orthogonal") { - return newState; - } - // Restore rotation because it might have been changed by the user - // in flight/oblique mode. Since this affects the matrix (which is - // also used in orthogonal mode), the rotation needs to be reset. - return setRotationReducer(newState, [0, 0, 0]); + return newState; } else { return state; } From d53dfdcf0fe4d4c5cb51c8086b3d83159e9ba616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 6 May 2025 15:41:16 +0200 Subject: [PATCH 002/128] fix shader --- frontend/javascripts/oxalis/shaders/coords.glsl.ts | 7 ++++--- frontend/javascripts/oxalis/shaders/filtering.glsl.ts | 1 + .../javascripts/oxalis/shaders/main_data_shaders.glsl.ts | 4 ++-- frontend/javascripts/oxalis/shaders/texture_access.glsl.ts | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/javascripts/oxalis/shaders/coords.glsl.ts b/frontend/javascripts/oxalis/shaders/coords.glsl.ts index 66b044c8447..3c00e0cbf7a 100644 --- a/frontend/javascripts/oxalis/shaders/coords.glsl.ts +++ b/frontend/javascripts/oxalis/shaders/coords.glsl.ts @@ -54,11 +54,12 @@ export const getWorldCoordUVW: ShaderModule = { // In orthogonal mode, the planes are offset in 3D space to allow skeletons to be rendered before // each plane. Since w (e.g., z for xy plane) is // the same for all texels computed in this shader, we simply use globalPosition[w] instead - <% if (isOrthogonal) { %> + // TODOM + //<% if (isOrthogonal) { %> getW(globalPosition) - <% } else { %> + //<% } else { %> worldCoordUVW.z / voxelSizeFactorUVW.z - <% } %> + //<% } %> ); return worldCoordUVW; diff --git a/frontend/javascripts/oxalis/shaders/filtering.glsl.ts b/frontend/javascripts/oxalis/shaders/filtering.glsl.ts index 47299b96a14..6de891705f7 100644 --- a/frontend/javascripts/oxalis/shaders/filtering.glsl.ts +++ b/frontend/javascripts/oxalis/shaders/filtering.glsl.ts @@ -79,6 +79,7 @@ export const getTrilinearColorFor: ShaderModule = { const getMaybeFilteredColor: ShaderModule = { requirements: [getColorForCoords, getBilinearColorFor, getTrilinearColorFor], code: ` + // TODOM: Consider passing an argument like "isRotated" to determine whether bilinear or trilinear filtering should be used. vec4 getMaybeFilteredColor( float layerIndex, float d_texture_width, diff --git a/frontend/javascripts/oxalis/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/oxalis/shaders/main_data_shaders.glsl.ts index 4c064bdcc27..723cc6a94fa 100644 --- a/frontend/javascripts/oxalis/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/oxalis/shaders/main_data_shaders.glsl.ts @@ -480,7 +480,7 @@ void main() { // Remember the original z position, since it can subtly diverge in the // following calculations due to floating point inaccuracies. This can // result in artifacts, such as the crosshair disappearing. - float originalZ = gl_Position.z; + /*float originalZ = gl_Position.z; // Remember, the top of the viewport has Y=1 whereas the left has X=-1. vec3 worldCoordTopLeft = transDim((modelMatrix * vec4(-PLANE_WIDTH/2., PLANE_WIDTH/2., 0., 1.)).xyz); @@ -590,7 +590,7 @@ void main() { } } } - <% }) %> + <% }) %>*/ } `)({ ...params, diff --git a/frontend/javascripts/oxalis/shaders/texture_access.glsl.ts b/frontend/javascripts/oxalis/shaders/texture_access.glsl.ts index 6a03dc8e990..bc784d84324 100644 --- a/frontend/javascripts/oxalis/shaders/texture_access.glsl.ts +++ b/frontend/javascripts/oxalis/shaders/texture_access.glsl.ts @@ -206,8 +206,8 @@ export const getColorForCoords: ShaderModule = { // To avoid rare rendering artifacts, don't use the precomputed // bucket address when being at the border of buckets. - bool beSafe = false; - { + bool beSafe = true; + /*{ renderedMagIdx = outputMagIdx[globalLayerIndex]; vec3 coords = floor(getAbsoluteCoords(worldPositionUVW, renderedMagIdx, globalLayerIndex)); vec3 absoluteBucketPosition = div(coords, bucketWidth); @@ -220,7 +220,7 @@ export const getColorForCoords: ShaderModule = { ) { beSafe = true; } - } + }*/ if (beSafe || !supportsPrecomputedBucketAddress) { From 53fe5736ff11603b71a2067c4e418617a369db97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 6 May 2025 17:21:34 +0200 Subject: [PATCH 003/128] WIP: make planes and cameras rotate accordingly --- frontend/javascripts/oxalis/api/api_latest.ts | 4 +-- .../oxalis/controller/camera_controller.ts | 12 ++++++- .../oxalis/controller/scene_controller.ts | 13 ++++--- .../oxalis/controller/url_manager.ts | 4 +-- .../viewmodes/arbitrary_controller.tsx | 4 +-- .../javascripts/oxalis/geometries/plane.ts | 4 +-- .../oxalis/model/accessors/flycam_accessor.ts | 34 ++++++++++++++++--- .../oxalis/model/helpers/nml_helpers.ts | 4 +-- .../model/sagas/skeletontracing_saga.ts | 4 +-- .../oxalis/model/sagas/volumetracing_saga.tsx | 4 +-- .../view/action-bar/dataset_position_view.tsx | 12 +++---- .../javascripts/oxalis/view/plane_view.ts | 20 +++++------ .../test/reducers/flycam_reducer.spec.ts | 14 ++++---- 13 files changed, 83 insertions(+), 50 deletions(-) diff --git a/frontend/javascripts/oxalis/api/api_latest.ts b/frontend/javascripts/oxalis/api/api_latest.ts index b5324775419..74715bba9af 100644 --- a/frontend/javascripts/oxalis/api/api_latest.ts +++ b/frontend/javascripts/oxalis/api/api_latest.ts @@ -52,7 +52,7 @@ import { flatToNestedMatrix } from "oxalis/model/accessors/dataset_layer_transfo import { getActiveMagIndexForLayer, getPosition, - getRotation, + getRotationInDegrees, } from "oxalis/model/accessors/flycam_accessor"; import { findTreeByNodeId, @@ -1396,7 +1396,7 @@ class TracingApi { ? dimensions.thirdDimensionForPlane(activeViewport) : null; const curPosition = getPosition(Store.getState().flycam); - const curRotation = getRotation(Store.getState().flycam); + const curRotation = getRotationInDegrees(Store.getState().flycam); if (!Array.isArray(rotation)) rotation = curRotation; rotation = this.getShortestRotation(curRotation, rotation); diff --git a/frontend/javascripts/oxalis/controller/camera_controller.ts b/frontend/javascripts/oxalis/controller/camera_controller.ts index 77aedd19d81..7f6a4608423 100644 --- a/frontend/javascripts/oxalis/controller/camera_controller.ts +++ b/frontend/javascripts/oxalis/controller/camera_controller.ts @@ -4,7 +4,7 @@ import _ from "lodash"; import type { OrthoView, OrthoViewMap, OrthoViewRects, Vector3 } from "oxalis/constants"; import { OrthoViewValuesWithoutTDView, OrthoViews } from "oxalis/constants"; import { getDatasetCenter, getDatasetExtentInUnit } from "oxalis/model/accessors/dataset_accessor"; -import { getPosition } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInDegrees, getRotationInRadian, getRotationInRadianFixed } from "oxalis/model/accessors/flycam_accessor"; import { getInputCatcherAspectRatio, getPlaneExtentInVoxelFromStore, @@ -18,6 +18,7 @@ import Store from "oxalis/store"; import * as React from "react"; import * as THREE from "three"; import TWEEN from "tween.js"; +import { OrthoBaseRotations } from "./scene_controller"; type Props = { cameras: OrthoViewMap; @@ -159,6 +160,15 @@ class CameraController extends React.PureComponent { this.props.cameras[OrthoViews.PLANE_XY].position.set(cPos[0], cPos[1], cPos[2]); this.props.cameras[OrthoViews.PLANE_YZ].position.set(cPos[0], cPos[1], cPos[2]); this.props.cameras[OrthoViews.PLANE_XZ].position.set(cPos[0], cPos[1], cPos[2]); + // Now set rotation for all cameras respecting the base rotation of each camera. + const gRot = getRotationInRadianFixed(state.flycam); + const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(gRot[0], gRot[1], gRot[2])); + const baseRotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_XY]); + const baseRotationMatrixYZ = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_YZ]); + const baseRotationMatrixXZ = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_XZ]); + this.props.cameras[OrthoViews.PLANE_XY].setRotationFromMatrix(rotationMatrix.multiply(baseRotationMatrixXY)); + this.props.cameras[OrthoViews.PLANE_YZ].setRotationFromMatrix(rotationMatrix.multiply(baseRotationMatrixYZ)); + this.props.cameras[OrthoViews.PLANE_XZ].setRotationFromMatrix(rotationMatrix.multiply(baseRotationMatrixXZ)); } bindToEvents() { diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 80f9bb2a7ef..6118a424d99 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -44,7 +44,7 @@ import { getTransformsForLayerOrNull, getTransformsForSkeletonLayer, } from "oxalis/model/accessors/dataset_layer_transformation_accessor"; -import { getActiveMagIndicesForLayers, getPosition } from "oxalis/model/accessors/flycam_accessor"; +import { getActiveMagIndicesForLayers, getPosition, getRotationInRadian } from "oxalis/model/accessors/flycam_accessor"; import { getSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor"; import { getSomeTracing, getTaskBoundingBoxes } from "oxalis/model/accessors/tracing_accessor"; import { getPlaneScalingFactor } from "oxalis/model/accessors/view_mode_accessor"; @@ -383,15 +383,16 @@ class SceneController { // This method is called for each of the four cams. Even // though they are all looking at the same scene, some // things have to be changed for each cam. + const {datasetConfiguration, userConfiguration, flycam} = Store.getState(); const { tdViewDisplayPlanes, tdViewDisplayDatasetBorders, tdViewDisplayLayerBorders } = - Store.getState().userConfiguration; + userConfiguration; // Only set the visibility of the dataset bounding box for the TDView. // This has to happen before updateForCam is called as otherwise cross section visibility // might be changed unintentionally. this.datasetBoundingBox.setVisibility(id !== OrthoViews.TDView || tdViewDisplayDatasetBorders); this.datasetBoundingBox.updateForCam(id); this.userBoundingBoxes.forEach((bbCube) => bbCube.updateForCam(id)); - const layerNameToIsDisabled = getLayerNameToIsDisabled(Store.getState().datasetConfiguration); + const layerNameToIsDisabled = getLayerNameToIsDisabled(datasetConfiguration); Object.keys(this.layerBoundingBoxes).forEach((layerName) => { const bbCube = this.layerBoundingBoxes[layerName]; const visible = @@ -409,7 +410,8 @@ class SceneController { this.annotationToolsGeometryGroup.visible = id !== OrthoViews.TDView; this.lineMeasurementGeometry.updateForCam(id); - const originalPosition = getPosition(Store.getState().flycam); + const originalPosition = getPosition(flycam); + const rotation = getRotationInRadian(flycam); if (id !== OrthoViews.TDView) { for (const planeId of OrthoViewValuesWithoutTDView) { if (planeId === id) { @@ -423,6 +425,7 @@ class SceneController { pos[ind[2]] += planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; this.planes[planeId].setPosition(pos, originalPosition); + this.planes[planeId].setRotation(new THREE.Euler(rotation[0], rotation[1], rotation[2])); this.quickSelectGeometry.adaptVisibilityForRendering(originalPosition, ind[2]); } else { @@ -448,7 +451,7 @@ class SceneController { const { flycam } = state; const globalPosition = getPosition(flycam); - const magIndices = getActiveMagIndicesForLayers(Store.getState()); + const magIndices = getActiveMagIndicesForLayers(state); for (const dataLayer of Model.getAllLayers()) { dataLayer.layerRenderingManager.updateDataTextures( globalPosition, diff --git a/frontend/javascripts/oxalis/controller/url_manager.ts b/frontend/javascripts/oxalis/controller/url_manager.ts index 6284b01015b..07079c393ae 100644 --- a/frontend/javascripts/oxalis/controller/url_manager.ts +++ b/frontend/javascripts/oxalis/controller/url_manager.ts @@ -8,7 +8,7 @@ import _ from "lodash"; import messages from "messages"; import type { Vector3, ViewMode } from "oxalis/constants"; import constants, { ViewModeValues, MappingStatusEnum } from "oxalis/constants"; -import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInDegrees } from "oxalis/model/accessors/flycam_accessor"; import { enforceSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor"; import { getMeshesForCurrentAdditionalCoordinates } from "oxalis/model/accessors/volumetracing_accessor"; import { @@ -286,7 +286,7 @@ class UrlManager { const zoomStep = Utils.roundTo(state.flycam.zoomStep, 3); const rotationOptional = constants.MODES_ARBITRARY.includes(mode) ? { - rotation: Utils.map3((e) => Utils.roundTo(e, 2), getRotation(state.flycam)), + rotation: Utils.map3((e) => Utils.roundTo(e, 2), getRotationInDegrees(state.flycam)), } : {}; const activeNode = state.annotation.skeleton?.activeNodeId; diff --git a/frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.tsx b/frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.tsx index 96b8076bb8c..379b41adffb 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.tsx +++ b/frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.tsx @@ -11,7 +11,7 @@ import getSceneController from "oxalis/controller/scene_controller_provider"; import TDController from "oxalis/controller/td_controller"; import ArbitraryPlane from "oxalis/geometries/arbitrary_plane"; import Crosshair from "oxalis/geometries/crosshair"; -import { getMoveOffset3d, getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; +import { getMoveOffset3d, getPosition, getRotationInDegrees } from "oxalis/model/accessors/flycam_accessor"; import { getActiveNode, getMaxNodeId, @@ -402,7 +402,7 @@ class ArbitraryController extends React.PureComponent { } const state = Store.getState(); const position = getPosition(state.flycam); - const rotation = getRotation(state.flycam); + const rotation = getRotationInDegrees(state.flycam); const additionalCoordinates = state.flycam.additionalCoordinates; Store.dispatch( createNodeAction( diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index 63812c4c919..3bd490138a3 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -99,7 +99,6 @@ class Plane { // render them AFTER the plane has been rendered. this.crosshair[i].renderOrder = 1; this.crosshair[i].name = `${this.planeID}-crosshair-${i}`; - this.crosshair[i].matrixAutoUpdate = false; } // create borders @@ -116,7 +115,6 @@ class Plane { this.getLineBasicMaterial(OrthoViewColors[this.planeID], 1), ); this.TDViewBorders.name = `${this.planeID}-TDViewBorders`; - this.TDViewBorders.matrixAutoUpdate = false; } setDisplayCrosshair = (value: boolean): void => { @@ -171,7 +169,7 @@ class Plane { setRotation = (rotVec: THREE.Euler): void => { const baseRotationMatrix = new THREE.Matrix4().makeRotationFromEuler(this.baseRotation); const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(rotVec); - const combinedMatrix = baseRotationMatrix.multiply(rotationMatrix); + const combinedMatrix = rotationMatrix.multiply(baseRotationMatrix); this.getMeshes().map((mesh) => mesh.setRotationFromMatrix(combinedMatrix), ); diff --git a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts index 1356722c843..9cb8a4254b9 100644 --- a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts @@ -297,18 +297,40 @@ function _getFlooredPosition(flycam: Flycam): Vector3 { return map3((x) => Math.floor(x), _getPosition(flycam)); } -function _getRotation(flycam: Flycam): Vector3 { +function _getRotationInRadianFixed(flycam: Flycam): Vector3 { + const object = new THREE.Object3D(); + const matrix = new THREE.Matrix4().fromArray(flycam.currentMatrix).transpose(); + object.applyMatrix4(matrix); + const rotation: Vector3 = [object.rotation.x, object.rotation.y - Math.PI, object.rotation.z]; + return [ + mod(rotation[0], Math.PI*2), + mod(rotation[1], Math.PI*2), + mod(rotation[2], Math.PI*2), + ]; +} + +function _getRotationInRadian(flycam: Flycam): Vector3 { const object = new THREE.Object3D(); const matrix = new THREE.Matrix4().fromArray(flycam.currentMatrix).transpose(); object.applyMatrix4(matrix); const rotation: Vector3 = [object.rotation.x, object.rotation.y, object.rotation.z - Math.PI]; return [ - mod((180 / Math.PI) * rotation[0], 360), - mod((180 / Math.PI) * rotation[1], 360), - mod((180 / Math.PI) * rotation[2], 360), + mod(rotation[0], Math.PI*2), + mod(rotation[1], Math.PI*2), + mod(rotation[2], Math.PI*2), ]; } +function _getRotationInDegrees(flycam: Flycam): Vector3 { + const rotationInRadian = getRotationInRadian(flycam); + // Modulo operation not needed as already done in getRotationInRadian. + return [ + (180 / Math.PI) * rotationInRadian[0], + (180 / Math.PI) * rotationInRadian[1], + (180 / Math.PI) * rotationInRadian[2], + ] +} + function _getZoomedMatrix(flycam: Flycam): Matrix4x4 { return M4x4.scale1(flycam.zoomStep, flycam.currentMatrix); } @@ -317,7 +339,9 @@ export const getUp = memoizeOne(_getUp); export const getLeft = memoizeOne(_getLeft); export const getPosition = memoizeOne(_getPosition); export const getFlooredPosition = memoizeOne(_getFlooredPosition); -export const getRotation = memoizeOne(_getRotation); +export const getRotationInRadianFixed = memoizeOne(_getRotationInRadianFixed); +export const getRotationInRadian = memoizeOne(_getRotationInRadian); +export const getRotationInDegrees = memoizeOne(_getRotationInDegrees); export const getZoomedMatrix = memoizeOne(_getZoomedMatrix); function _getActiveMagIndicesForLayers(state: WebknossosState): { [layerName: string]: number } { diff --git a/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts b/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts index abbe371ee1e..da970d011da 100644 --- a/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts +++ b/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts @@ -13,7 +13,7 @@ import { type Vector3, } from "oxalis/constants"; import Constants from "oxalis/constants"; -import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInDegrees } from "oxalis/model/accessors/flycam_accessor"; import EdgeCollection from "oxalis/model/edge_collection"; import { getMaximumGroupId, @@ -242,7 +242,7 @@ function serializeParameters( const editPositionAdditionalCoordinates = state.flycam.additionalCoordinates; const { additionalAxes } = skeletonTracing; - const editRotation = getRotation(state.flycam); + const editRotation = getRotationInDegrees(state.flycam); const userBBoxes = skeletonTracing.userBoundingBoxes; const taskBB = skeletonTracing.boundingBox; return [ diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index 10de5421181..3119476edc2 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -12,7 +12,7 @@ import memoizeOne from "memoize-one"; import messages from "messages"; import { TreeTypeEnum } from "oxalis/constants"; import { getLayerByName } from "oxalis/model/accessors/dataset_accessor"; -import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInDegrees } from "oxalis/model/accessors/flycam_accessor"; import { enforceSkeletonTracing, findTreeByName, @@ -632,7 +632,7 @@ export function* diffSkeletonTracing( skeletonTracing, V3.floor(getPosition(flycam)), flycam.additionalCoordinates, - getRotation(flycam), + getRotationInDegrees(flycam), flycam.zoomStep, ); } diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index 8e1654d017c..fddc087aeed 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -14,7 +14,7 @@ import { getSupportedValueRangeOfLayer, isInSupportedValueRangeForLayer, } from "oxalis/model/accessors/dataset_accessor"; -import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInDegrees } from "oxalis/model/accessors/flycam_accessor"; import { isBrushTool, isTraceTool, @@ -474,7 +474,7 @@ export function* diffVolumeTracing( volumeTracing, V3.floor(getPosition(flycam)), flycam.additionalCoordinates, - getRotation(flycam), + getRotationInDegrees(flycam), flycam.zoomStep, ); } diff --git a/frontend/javascripts/oxalis/view/action-bar/dataset_position_view.tsx b/frontend/javascripts/oxalis/view/action-bar/dataset_position_view.tsx index ee4a77145f6..273b9094c77 100644 --- a/frontend/javascripts/oxalis/view/action-bar/dataset_position_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/dataset_position_view.tsx @@ -6,9 +6,8 @@ import Toast from "libs/toast"; import { Vector3Input } from "libs/vector_input"; import message from "messages"; import type { Vector3, ViewMode } from "oxalis/constants"; -import constants from "oxalis/constants"; import { getDatasetExtentInVoxel } from "oxalis/model/accessors/dataset_accessor"; -import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInDegrees } from "oxalis/model/accessors/flycam_accessor"; import { setPositionAction, setRotationAction } from "oxalis/model/actions/flycam_actions"; import type { Flycam, Task, WebknossosState } from "oxalis/store"; import Store from "oxalis/store"; @@ -50,7 +49,7 @@ class DatasetPositionView extends PureComponent { }; copyRotationToClipboard = async () => { - const rotation = V3.round(getRotation(this.props.flycam)).join(", "); + const rotation = V3.round(getRotationInDegrees(this.props.flycam)).join(", "); await navigator.clipboard.writeText(rotation); Toast.success("Rotation copied to clipboard"); }; @@ -111,8 +110,7 @@ class DatasetPositionView extends PureComponent { maybeErrorMessage = message["tracing.out_of_task_bounds"]; } - const rotation = V3.round(getRotation(this.props.flycam)); - const isArbitraryMode = constants.MODES_ARBITRARY.includes(this.props.viewMode); + const rotation = V3.round(getRotationInDegrees(this.props.flycam)); const positionView = (
{ /> - {isArbitraryMode ? ( + {( { allowDecimals /> - ) : null} + ) }
); return ( diff --git a/frontend/javascripts/oxalis/view/plane_view.ts b/frontend/javascripts/oxalis/view/plane_view.ts index 0585e707ae4..3b531f4e537 100644 --- a/frontend/javascripts/oxalis/view/plane_view.ts +++ b/frontend/javascripts/oxalis/view/plane_view.ts @@ -104,15 +104,15 @@ class PlaneView { // This is the main render function. // All 3D meshes and the trianglesplane are rendered here. TWEEN.update(); - const SceneController = getSceneController(); + const sceneController = getSceneController(); // skip rendering if nothing has changed // This prevents the GPU/CPU from constantly // working and keeps your lap cool // ATTENTION: this limits the FPS to 60 FPS (depending on the keypress update frequency) if (forceRender || this.needsRerender) { - const { renderer, scene } = SceneController; - SceneController.update(); + const { renderer, scene } = sceneController; + sceneController.update(); const storeState = Store.getState(); const viewport = { [OrthoViews.PLANE_XY]: getInputCatcherRect(storeState, "PLANE_XY"), @@ -124,7 +124,7 @@ class PlaneView { clearCanvas(renderer); for (const plane of OrthoViewValues) { - SceneController.updateSceneForCam(plane); + sceneController.updateSceneForCam(plane); const { left, top, width, height } = viewport[plane]; if (width > 0 && height > 0) { @@ -139,8 +139,8 @@ class PlaneView { performMeshHitTest = _.throttle((mousePosition: [number, number]): RaycasterHit => { const storeState = Store.getState(); - const SceneController = getSceneController(); - const { segmentMeshController } = SceneController; + const sceneController = getSceneController(); + const { segmentMeshController } = sceneController; const { meshesLayerLODRootGroup } = segmentMeshController; const tdViewport = getInputCatcherRect(storeState, "TDView"); const { hoveredSegmentId } = storeState.temporaryConfiguration; @@ -230,8 +230,8 @@ class PlaneView { clearLastMeshHitTest = () => { if (oldRaycasterHit?.node.parent != null) { - const SceneController = getSceneController(); - const { segmentMeshController } = SceneController; + const sceneController = getSceneController(); + const { segmentMeshController } = sceneController; segmentMeshController.updateMeshAppearance(oldRaycasterHit.node, false, undefined, null); oldRaycasterHit = null; } @@ -275,8 +275,8 @@ class PlaneView { } start(): void { - const SceneController = getSceneController(); - const { segmentMeshController } = SceneController; + const sceneController = getSceneController(); + const { segmentMeshController } = sceneController; this.unsubscribeFunctions.push( app.vent.on("rerender", () => { diff --git a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts index 9b2c623c2c8..06d9449125c 100644 --- a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts @@ -4,7 +4,7 @@ import { UnitLong, OrthoViews } from "oxalis/constants"; import update from "immutability-helper"; import { getPosition, - getRotation, + getRotationInDegrees, getUp, getLeft, getZoomedMatrix, @@ -73,7 +73,7 @@ describe("Flycam", () => { it("should set the rotation the flycam", () => { const rotateAction = FlycamActions.setRotationAction([180, 0, 0]); const newState = FlycamReducer(initialState, rotateAction); - equalWithEpsilon(getRotation(newState.flycam), [180, 0, 0]); + equalWithEpsilon(getRotationInDegrees(newState.flycam), [180, 0, 0]); equalWithEpsilon(getUp(newState.flycam), [0, 1, -0]); equalWithEpsilon(getLeft(newState.flycam), [-1, 0, 0]); }); @@ -100,33 +100,33 @@ describe("Flycam", () => { const rotateAction = FlycamActions.rotateFlycamAction(0.5 * Math.PI, [1, 1, 0]); const newState = FlycamReducer(initialState, rotateAction); equalWithEpsilon(getPosition(newState.flycam), [0, 0, 0]); - equalWithEpsilon(V3.floor(getRotation(newState.flycam)), [270, 315, 135]); + equalWithEpsilon(V3.floor(getRotationInDegrees(newState.flycam)), [270, 315, 135]); }); it("should pitch the flycam", () => { const rotateAction = FlycamActions.pitchFlycamAction(0.5 * Math.PI); const newState = FlycamReducer(initialState, rotateAction); equalWithEpsilon(getPosition(newState.flycam), [0, 0, 0]); - equalWithEpsilon(getRotation(newState.flycam), [270, 0, 180]); + equalWithEpsilon(getRotationInDegrees(newState.flycam), [270, 0, 180]); }); it("should pitch the flycam with spherical cap radius", () => { const rotateAction = FlycamActions.pitchFlycamAction(0.5 * Math.PI, true); const newState = FlycamReducer(initialState, rotateAction); equalWithEpsilon(getPosition(newState.flycam), [0, -200, -200]); - equalWithEpsilon(getRotation(newState.flycam), [270, 0, 180]); + equalWithEpsilon(getRotationInDegrees(newState.flycam), [270, 0, 180]); }); it("should yaw the flycam", () => { const rotateAction = FlycamActions.yawFlycamAction(0.5 * Math.PI); const newState = FlycamReducer(initialState, rotateAction); - equalWithEpsilon(getRotation(newState.flycam), [0, 270, 180]); + equalWithEpsilon(getRotationInDegrees(newState.flycam), [0, 270, 180]); }); it("should roll the flycam", () => { const rotateAction = FlycamActions.rollFlycamAction(0.5 * Math.PI); const newState = FlycamReducer(initialState, rotateAction); - equalWithEpsilon(getRotation(newState.flycam), [0, 0, 90]); + equalWithEpsilon(getRotationInDegrees(newState.flycam), [0, 0, 90]); }); it("should move in ortho mode", () => { From a0effbaed48eeb2a073db7e5ca80c0ff29ee7567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 6 May 2025 18:20:47 +0200 Subject: [PATCH 004/128] fix base rotation of xy viewport --- frontend/javascripts/oxalis/controller/scene_controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 6118a424d99..9b8defa2ed6 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -70,7 +70,7 @@ const CUBE_COLOR = 0x999999; const LAYER_CUBE_COLOR = 0xffff99; export const OrthoBaseRotations = { - [OrthoViews.PLANE_XY]: new THREE.Euler(0, Math.PI, 0), + [OrthoViews.PLANE_XY]: new THREE.Euler(Math.PI, 0, 0), [OrthoViews.PLANE_YZ]: new THREE.Euler(Math.PI, (1 / 2) * Math.PI, 0), [OrthoViews.PLANE_XZ]: new THREE.Euler((-1 / 2) * Math.PI, 0, 0), [OrthoViews.TDView]: new THREE.Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4), From 46962429ff107d0aabc4745835ee13451954757f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 6 May 2025 18:21:17 +0200 Subject: [PATCH 005/128] fix rotation of xz and yz viewports (three js multiplys matrices in place :/) --- .../oxalis/controller/camera_controller.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/camera_controller.ts b/frontend/javascripts/oxalis/controller/camera_controller.ts index 7f6a4608423..2a6a0210261 100644 --- a/frontend/javascripts/oxalis/controller/camera_controller.ts +++ b/frontend/javascripts/oxalis/controller/camera_controller.ts @@ -161,14 +161,20 @@ class CameraController extends React.PureComponent { this.props.cameras[OrthoViews.PLANE_YZ].position.set(cPos[0], cPos[1], cPos[2]); this.props.cameras[OrthoViews.PLANE_XZ].position.set(cPos[0], cPos[1], cPos[2]); // Now set rotation for all cameras respecting the base rotation of each camera. - const gRot = getRotationInRadianFixed(state.flycam); - const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(gRot[0], gRot[1], gRot[2])); + const gRot = getRotationInRadian(state.flycam); + // Copies are needed because multiply modifies the matrix in-place. + const rotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(gRot[0], gRot[1], gRot[2])); + const rotationMatrixYZ = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(gRot[0], gRot[1], gRot[2])); + const rotationMatrixXZ = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(gRot[0], gRot[1], gRot[2])); const baseRotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_XY]); const baseRotationMatrixYZ = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_YZ]); const baseRotationMatrixXZ = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_XZ]); - this.props.cameras[OrthoViews.PLANE_XY].setRotationFromMatrix(rotationMatrix.multiply(baseRotationMatrixXY)); - this.props.cameras[OrthoViews.PLANE_YZ].setRotationFromMatrix(rotationMatrix.multiply(baseRotationMatrixYZ)); - this.props.cameras[OrthoViews.PLANE_XZ].setRotationFromMatrix(rotationMatrix.multiply(baseRotationMatrixXZ)); + this.props.cameras[OrthoViews.PLANE_XY].setRotationFromMatrix(rotationMatrixXY.multiply(baseRotationMatrixXY)); + this.props.cameras[OrthoViews.PLANE_YZ].setRotationFromMatrix(rotationMatrixYZ.multiply(baseRotationMatrixYZ)); + this.props.cameras[OrthoViews.PLANE_XZ].setRotationFromMatrix(rotationMatrixXZ.multiply(baseRotationMatrixXZ)); + this.props.cameras[OrthoViews.PLANE_XY].updateProjectionMatrix(); + this.props.cameras[OrthoViews.PLANE_YZ].updateProjectionMatrix(); + this.props.cameras[OrthoViews.PLANE_XZ].updateProjectionMatrix(); } bindToEvents() { From 1a98739b78cf57d6b65505ab3aee3b3ee30ec6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 7 May 2025 13:19:37 +0200 Subject: [PATCH 006/128] little cleanup & WIP fixing rendering issues --- .../oxalis/controller/camera_controller.ts | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/camera_controller.ts b/frontend/javascripts/oxalis/controller/camera_controller.ts index 2a6a0210261..c586f768489 100644 --- a/frontend/javascripts/oxalis/controller/camera_controller.ts +++ b/frontend/javascripts/oxalis/controller/camera_controller.ts @@ -4,7 +4,7 @@ import _ from "lodash"; import type { OrthoView, OrthoViewMap, OrthoViewRects, Vector3 } from "oxalis/constants"; import { OrthoViewValuesWithoutTDView, OrthoViews } from "oxalis/constants"; import { getDatasetCenter, getDatasetExtentInUnit } from "oxalis/model/accessors/dataset_accessor"; -import { getPosition, getRotationInDegrees, getRotationInRadian, getRotationInRadianFixed } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInRadian } from "oxalis/model/accessors/flycam_accessor"; import { getInputCatcherAspectRatio, getPlaneExtentInVoxelFromStore, @@ -129,7 +129,7 @@ class CameraController extends React.PureComponent { // of clippingDistance. Theoretically, `far` could be set here too, however, // this leads to imprecision related bugs which cause the planes to not render // for certain clippingDistance values. - this.props.cameras[planeId].near = -clippingDistance; + this.props.cameras[planeId].near = -1000000000000; this.props.cameras[planeId].updateProjectionMatrix(); } @@ -163,15 +163,33 @@ class CameraController extends React.PureComponent { // Now set rotation for all cameras respecting the base rotation of each camera. const gRot = getRotationInRadian(state.flycam); // Copies are needed because multiply modifies the matrix in-place. - const rotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(gRot[0], gRot[1], gRot[2])); - const rotationMatrixYZ = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(gRot[0], gRot[1], gRot[2])); - const rotationMatrixXZ = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(gRot[0], gRot[1], gRot[2])); - const baseRotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_XY]); - const baseRotationMatrixYZ = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_YZ]); - const baseRotationMatrixXZ = new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[OrthoViews.PLANE_XZ]); - this.props.cameras[OrthoViews.PLANE_XY].setRotationFromMatrix(rotationMatrixXY.multiply(baseRotationMatrixXY)); - this.props.cameras[OrthoViews.PLANE_YZ].setRotationFromMatrix(rotationMatrixYZ.multiply(baseRotationMatrixYZ)); - this.props.cameras[OrthoViews.PLANE_XZ].setRotationFromMatrix(rotationMatrixXZ.multiply(baseRotationMatrixXZ)); + const rotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler( + new THREE.Euler(gRot[0], gRot[1], gRot[2]), + ); + const rotationMatrixYZ = new THREE.Matrix4().makeRotationFromEuler( + new THREE.Euler(gRot[0], gRot[1], gRot[2]), + ); + const rotationMatrixXZ = new THREE.Matrix4().makeRotationFromEuler( + new THREE.Euler(gRot[0], gRot[1], gRot[2]), + ); + const baseRotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler( + OrthoBaseRotations[OrthoViews.PLANE_XY], + ); + const baseRotationMatrixYZ = new THREE.Matrix4().makeRotationFromEuler( + OrthoBaseRotations[OrthoViews.PLANE_YZ], + ); + const baseRotationMatrixXZ = new THREE.Matrix4().makeRotationFromEuler( + OrthoBaseRotations[OrthoViews.PLANE_XZ], + ); + this.props.cameras[OrthoViews.PLANE_XY].setRotationFromMatrix( + rotationMatrixXY.multiply(baseRotationMatrixXY), + ); + this.props.cameras[OrthoViews.PLANE_YZ].setRotationFromMatrix( + rotationMatrixYZ.multiply(baseRotationMatrixYZ), + ); + this.props.cameras[OrthoViews.PLANE_XZ].setRotationFromMatrix( + rotationMatrixXZ.multiply(baseRotationMatrixXZ), + ); this.props.cameras[OrthoViews.PLANE_XY].updateProjectionMatrix(); this.props.cameras[OrthoViews.PLANE_YZ].updateProjectionMatrix(); this.props.cameras[OrthoViews.PLANE_XZ].updateProjectionMatrix(); From e9a91cad10de55131f83acc384c70497d0972bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 7 May 2025 17:14:33 +0200 Subject: [PATCH 007/128] WIP fixing plane scaling --- .../oxalis/controller/camera_controller.ts | 2 +- .../oxalis/controller/scene_controller.ts | 18 +++++++---- .../javascripts/oxalis/geometries/plane.ts | 32 +++++++++++++------ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/camera_controller.ts b/frontend/javascripts/oxalis/controller/camera_controller.ts index c586f768489..227c11af652 100644 --- a/frontend/javascripts/oxalis/controller/camera_controller.ts +++ b/frontend/javascripts/oxalis/controller/camera_controller.ts @@ -129,7 +129,7 @@ class CameraController extends React.PureComponent { // of clippingDistance. Theoretically, `far` could be set here too, however, // this leads to imprecision related bugs which cause the planes to not render // for certain clippingDistance values. - this.props.cameras[planeId].near = -1000000000000; + this.props.cameras[planeId].near = -clippingDistance; this.props.cameras[planeId].updateProjectionMatrix(); } diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 9b8defa2ed6..2e3c04281f0 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -44,7 +44,11 @@ import { getTransformsForLayerOrNull, getTransformsForSkeletonLayer, } from "oxalis/model/accessors/dataset_layer_transformation_accessor"; -import { getActiveMagIndicesForLayers, getPosition, getRotationInRadian } from "oxalis/model/accessors/flycam_accessor"; +import { + getActiveMagIndicesForLayers, + getPosition, + getRotationInRadian, +} from "oxalis/model/accessors/flycam_accessor"; import { getSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor"; import { getSomeTracing, getTaskBoundingBoxes } from "oxalis/model/accessors/tracing_accessor"; import { getPlaneScalingFactor } from "oxalis/model/accessors/view_mode_accessor"; @@ -76,12 +80,10 @@ export const OrthoBaseRotations = { [OrthoViews.TDView]: new THREE.Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4), }; - const getVisibleSegmentationLayerNames = reuseInstanceOnEquality((storeState: WebknossosState) => getVisibleSegmentationLayers(storeState).map((l) => l.name), ); - class SceneController { skeletons: Record = {}; isPlaneVisible: OrthoViewMap; @@ -383,9 +385,9 @@ class SceneController { // This method is called for each of the four cams. Even // though they are all looking at the same scene, some // things have to be changed for each cam. - const {datasetConfiguration, userConfiguration, flycam} = Store.getState(); + const { datasetConfiguration, userConfiguration, flycam } = Store.getState(); const { tdViewDisplayPlanes, tdViewDisplayDatasetBorders, tdViewDisplayLayerBorders } = - userConfiguration; + userConfiguration; // Only set the visibility of the dataset bounding box for the TDView. // This has to happen before updateForCam is called as otherwise cross section visibility // might be changed unintentionally. @@ -438,8 +440,10 @@ class SceneController { this.planes[planeId].setPosition(originalPosition); this.planes[planeId].setGrayCrosshairColor(); this.planes[planeId].setVisible( - tdViewDisplayPlanes !== TDViewDisplayModeEnum.NONE, - this.isPlaneVisible[planeId] && tdViewDisplayPlanes === TDViewDisplayModeEnum.DATA, + planeId === OrthoViews.PLANE_XZ && tdViewDisplayPlanes !== TDViewDisplayModeEnum.NONE, + planeId === OrthoViews.PLANE_XZ && + this.isPlaneVisible[planeId] && + tdViewDisplayPlanes === TDViewDisplayModeEnum.DATA, ); this.planes[planeId].materialFactory.uniforms.is3DViewBeingRendered.value = true; } diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index 3bd490138a3..e869c0664b3 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -4,9 +4,11 @@ import constants, { OrthoViewColors, OrthoViewCrosshairColors, OrthoViewGrayCrosshairColor, + OrthoViews, OrthoViewValues, } from "oxalis/constants"; import PlaneMaterialFactory from "oxalis/geometries/materials/plane_material_factory"; +import { getRotationInRadian } from "oxalis/model/accessors/flycam_accessor"; import Dimensions from "oxalis/model/dimensions"; import { getBaseVoxelFactorsInUnit } from "oxalis/model/scaleinfo"; import Store from "oxalis/store"; @@ -35,7 +37,7 @@ class Plane { planeID: OrthoView; materialFactory!: PlaneMaterialFactory; displayCrosshair: boolean; - baseScaleVector: THREE.Vector3; + baseScale: THREE.Vector3; // @ts-expect-error ts-migrate(2564) FIXME: Property 'crosshair' has no initializer and is not... Remove this comment to see the full error message crosshair: Array; // @ts-expect-error ts-migrate(2564) FIXME: Property 'TDViewBorders' has no initializer and is... Remove this comment to see the full error message @@ -53,7 +55,7 @@ class Plane { // --> scaleInfo.baseVoxel const baseVoxelFactors = getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale); const scaleArray = Dimensions.transDim(baseVoxelFactors, this.planeID); - this.baseScaleVector = new THREE.Vector3(...scaleArray); + this.baseScale = new THREE.Vector3(...scaleArray); this.baseRotation = new THREE.Euler(0, 0, 0); this.createMeshes(); } @@ -149,17 +151,29 @@ class Plane { if (this.lastScaleFactors[0] === xFactor && this.lastScaleFactors[1] === yFactor) { return; } + // TODOM: This is broken when rotation is active and needs to be fixed!!!! this.lastScaleFactors[0] = xFactor; this.lastScaleFactors[1] = yFactor; const scaleVec = new THREE.Vector3().multiplyVectors( new THREE.Vector3(xFactor, yFactor, 1), - this.baseScaleVector, + this.baseScale, ); - this.plane.scale.copy(scaleVec); - this.TDViewBorders.scale.copy(scaleVec); - this.crosshair[0].scale.copy(scaleVec); - this.crosshair[1].scale.copy(scaleVec); + this.getMeshes().map((mesh) => mesh.scale.copy(scaleVec)); + } + + private recalculateScale(): void { + // The baseScaleVector needs to be rotated like the current rotation settings of the plane to calculate the correct scaling vector. + const baseScaleCopy = new THREE.Vector3().copy(this.baseScale); + const rotation = getRotationInRadian(Store.getState().flycam); + //const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(...rotation)); + //baseScaleCopy.applyMatrix4(rotationMatrix); + const rotatedBaseScale = baseScaleCopy.applyEuler(this.plane.rotation); + const scaleVec = new THREE.Vector3().multiplyVectors( + new THREE.Vector3(this.lastScaleFactors[0], this.lastScaleFactors[1], 1), + rotatedBaseScale, + ); + this.getMeshes().map((mesh) => mesh.scale.copy(scaleVec)); } setBaseRotation = (rotVec: THREE.Euler): void => { @@ -170,9 +184,7 @@ class Plane { const baseRotationMatrix = new THREE.Matrix4().makeRotationFromEuler(this.baseRotation); const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(rotVec); const combinedMatrix = rotationMatrix.multiply(baseRotationMatrix); - this.getMeshes().map((mesh) => - mesh.setRotationFromMatrix(combinedMatrix), - ); + this.getMeshes().map((mesh) => mesh.setRotationFromMatrix(combinedMatrix)); }; // In case the plane's position was offset to make geometries From 4d46f9ccbfee668931bde3367f53fa00a6db7b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 8 May 2025 15:35:13 +0200 Subject: [PATCH 008/128] try to fix plane scale --- .../oxalis/controller/scene_controller.ts | 12 ++--- .../javascripts/oxalis/geometries/plane.ts | 52 ++++++++++++------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 2e3c04281f0..6a558a3d539 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -424,8 +424,10 @@ class SceneController { const ind = Dimensions.getIndices(planeId); // Offset the plane so the user can see the skeletonTracing behind the plane - pos[ind[2]] += - planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; + // TODOM: This concept is kinda broken when rotations are turned on. TODO investigate how to implement this instead. + //pos[ind[2]] += planeId === OrthoViews.PLANE_XY ? 1 : -1; + //pos[ind[2]] += + // planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; this.planes[planeId].setPosition(pos, originalPosition); this.planes[planeId].setRotation(new THREE.Euler(rotation[0], rotation[1], rotation[2])); @@ -440,10 +442,8 @@ class SceneController { this.planes[planeId].setPosition(originalPosition); this.planes[planeId].setGrayCrosshairColor(); this.planes[planeId].setVisible( - planeId === OrthoViews.PLANE_XZ && tdViewDisplayPlanes !== TDViewDisplayModeEnum.NONE, - planeId === OrthoViews.PLANE_XZ && - this.isPlaneVisible[planeId] && - tdViewDisplayPlanes === TDViewDisplayModeEnum.DATA, + tdViewDisplayPlanes !== TDViewDisplayModeEnum.NONE, + this.isPlaneVisible[planeId] && tdViewDisplayPlanes === TDViewDisplayModeEnum.DATA, ); this.planes[planeId].materialFactory.uniforms.is3DViewBeingRendered.value = true; } diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index e869c0664b3..0e49d7a46e8 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -4,12 +4,9 @@ import constants, { OrthoViewColors, OrthoViewCrosshairColors, OrthoViewGrayCrosshairColor, - OrthoViews, OrthoViewValues, } from "oxalis/constants"; import PlaneMaterialFactory from "oxalis/geometries/materials/plane_material_factory"; -import { getRotationInRadian } from "oxalis/model/accessors/flycam_accessor"; -import Dimensions from "oxalis/model/dimensions"; import { getBaseVoxelFactorsInUnit } from "oxalis/model/scaleinfo"; import Store from "oxalis/store"; import * as THREE from "three"; @@ -37,13 +34,14 @@ class Plane { planeID: OrthoView; materialFactory!: PlaneMaterialFactory; displayCrosshair: boolean; - baseScale: THREE.Vector3; + worldScale: THREE.Vector3; // @ts-expect-error ts-migrate(2564) FIXME: Property 'crosshair' has no initializer and is not... Remove this comment to see the full error message crosshair: Array; // @ts-expect-error ts-migrate(2564) FIXME: Property 'TDViewBorders' has no initializer and is... Remove this comment to see the full error message TDViewBorders: THREE.Line; lastScaleFactors: [number, number]; baseRotation: THREE.Euler; + outerGroup: THREE.Group; constructor(planeID: OrthoView) { this.planeID = planeID; @@ -54,9 +52,9 @@ class Plane { // is smaller in voxels, so that it is squared in nm. // --> scaleInfo.baseVoxel const baseVoxelFactors = getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale); - const scaleArray = Dimensions.transDim(baseVoxelFactors, this.planeID); - this.baseScale = new THREE.Vector3(...scaleArray); + this.worldScale = new THREE.Vector3(...baseVoxelFactors); this.baseRotation = new THREE.Euler(0, 0, 0); + this.outerGroup = new THREE.Group(); this.createMeshes(); } @@ -117,6 +115,17 @@ class Plane { this.getLineBasicMaterial(OrthoViewColors[this.planeID], 1), ); this.TDViewBorders.name = `${this.planeID}-TDViewBorders`; + + // For world scale (1, 1, 4), apply inverse to neutralize it in geometry + const invWorldScale = new THREE.Vector3( + 1 / this.worldScale.x, + 1 / this.worldScale.y, + 1 / this.worldScale.z, + ); + + this.plane.geometry.applyMatrix4( + new THREE.Matrix4().makeScale(invWorldScale.x, invWorldScale.y, invWorldScale.z), + ); } setDisplayCrosshair = (value: boolean): void => { @@ -154,25 +163,31 @@ class Plane { // TODOM: This is broken when rotation is active and needs to be fixed!!!! this.lastScaleFactors[0] = xFactor; this.lastScaleFactors[1] = yFactor; - - const scaleVec = new THREE.Vector3().multiplyVectors( - new THREE.Vector3(xFactor, yFactor, 1), - this.baseScale, - ); - this.getMeshes().map((mesh) => mesh.scale.copy(scaleVec)); + this.recalculateScale(); } private recalculateScale(): void { // The baseScaleVector needs to be rotated like the current rotation settings of the plane to calculate the correct scaling vector. - const baseScaleCopy = new THREE.Vector3().copy(this.baseScale); - const rotation = getRotationInRadian(Store.getState().flycam); - //const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(...rotation)); - //baseScaleCopy.applyMatrix4(rotationMatrix); - const rotatedBaseScale = baseScaleCopy.applyEuler(this.plane.rotation); + const localScaleFactorX = new THREE.Vector3(1, 0, 0); + const localScaleFactorY = new THREE.Vector3(0, 1, 0); + // Apply current rotation of the plane. + const localScaleFactorXRotated = localScaleFactorX.applyEuler(this.plane.rotation); + const localScaleFactorYRotated = localScaleFactorY.applyEuler(this.plane.rotation); + // Put scale measuring vectors into world scale to get distortion cause by potentially anisotropic voxel scale factor. + const scaleFactorXStrechedInSpace = localScaleFactorXRotated.multiply(this.worldScale); + const scaleFactorYStrechedInSpace = localScaleFactorYRotated.multiply(this.worldScale); + // Measure visual length (how much stretch the voxel scale factor causes) + const lengthX = scaleFactorXStrechedInSpace.length(); + const lengthY = scaleFactorYStrechedInSpace.length(); + // Calculate correction factors to neutralize the stretching caused by the scale factor. + const correctionX = lengthX / 1; + const correctionY = lengthY / 1; + const scaleVec = new THREE.Vector3().multiplyVectors( new THREE.Vector3(this.lastScaleFactors[0], this.lastScaleFactors[1], 1), - rotatedBaseScale, + new THREE.Vector3(correctionX, correctionY, 1).multiply(this.worldScale), ); + if (this.planeID === "PLANE_XZ") console.log("calculated scale", this.planeID, scaleVec); this.getMeshes().map((mesh) => mesh.scale.copy(scaleVec)); } @@ -185,6 +200,7 @@ class Plane { const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(rotVec); const combinedMatrix = rotationMatrix.multiply(baseRotationMatrix); this.getMeshes().map((mesh) => mesh.setRotationFromMatrix(combinedMatrix)); + this.recalculateScale(); }; // In case the plane's position was offset to make geometries From a645251eafaff248fa2807be557327aa6498e480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 9 May 2025 11:16:02 +0200 Subject: [PATCH 009/128] remove plane wrapping group again --- .../javascripts/oxalis/geometries/plane.ts | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index 0e49d7a46e8..4376ca15de4 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -41,7 +41,6 @@ class Plane { TDViewBorders: THREE.Line; lastScaleFactors: [number, number]; baseRotation: THREE.Euler; - outerGroup: THREE.Group; constructor(planeID: OrthoView) { this.planeID = planeID; @@ -54,7 +53,6 @@ class Plane { const baseVoxelFactors = getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale); this.worldScale = new THREE.Vector3(...baseVoxelFactors); this.baseRotation = new THREE.Euler(0, 0, 0); - this.outerGroup = new THREE.Group(); this.createMeshes(); } @@ -62,6 +60,7 @@ class Plane { const pWidth = constants.VIEWPORT_WIDTH; // create plane const planeGeo = new THREE.PlaneGeometry(pWidth, pWidth, PLANE_SUBDIVISION, PLANE_SUBDIVISION); + this.materialFactory = new PlaneMaterialFactory( this.planeID, false, @@ -71,13 +70,11 @@ class Plane { this.plane = new THREE.Mesh(planeGeo, textureMaterial); this.plane.name = `${this.planeID}-plane`; this.plane.material.side = THREE.DoubleSide; - // create crosshair - const crosshairGeometries = []; - this.crosshair = new Array(2); + // Create crosshairs + this.crosshair = new Array(2); for (let i = 0; i <= 1; i++) { - crosshairGeometries.push(new THREE.BufferGeometry()); - + const crosshairGeometry = new THREE.BufferGeometry(); // biome-ignore format: don't format array const crosshairVertices = new Float32Array([ (-pWidth / 2) * i, (-pWidth / 2) * (1 - i), 0, @@ -85,13 +82,10 @@ class Plane { 25 * i, 25 * (1 - i), 0, (pWidth / 2) * i, (pWidth / 2) * (1 - i), 0, ]); + crosshairGeometry.setAttribute("position", new THREE.BufferAttribute(crosshairVertices, 3)); - crosshairGeometries[i].setAttribute( - "position", - new THREE.BufferAttribute(crosshairVertices, 3), - ); this.crosshair[i] = new THREE.LineSegments( - crosshairGeometries[i], + crosshairGeometry, this.getLineBasicMaterial(OrthoViewCrosshairColors[this.planeID][i], 1), ); // Objects are rendered according to their renderOrder (lowest to highest). @@ -101,31 +95,21 @@ class Plane { this.crosshair[i].name = `${this.planeID}-crosshair-${i}`; } - // create borders - const vertices = []; - vertices.push(new THREE.Vector3(-pWidth / 2, -pWidth / 2, 0)); - vertices.push(new THREE.Vector3(-pWidth / 2, pWidth / 2, 0)); - vertices.push(new THREE.Vector3(pWidth / 2, pWidth / 2, 0)); - vertices.push(new THREE.Vector3(pWidth / 2, -pWidth / 2, 0)); - vertices.push(new THREE.Vector3(-pWidth / 2, -pWidth / 2, 0)); - const tdViewBordersGeo = new THREE.BufferGeometry().setFromPoints(vertices); + // Create borders + const vertices = [ + new THREE.Vector3(-pWidth / 2, -pWidth / 2, 0), + new THREE.Vector3(-pWidth / 2, pWidth / 2, 0), + new THREE.Vector3(pWidth / 2, pWidth / 2, 0), + new THREE.Vector3(pWidth / 2, -pWidth / 2, 0), + new THREE.Vector3(-pWidth / 2, -pWidth / 2, 0), + ]; + const tdBorderGeometry = new THREE.BufferGeometry().setFromPoints(vertices); this.TDViewBorders = new THREE.Line( - tdViewBordersGeo, + tdBorderGeometry, this.getLineBasicMaterial(OrthoViewColors[this.planeID], 1), ); this.TDViewBorders.name = `${this.planeID}-TDViewBorders`; - - // For world scale (1, 1, 4), apply inverse to neutralize it in geometry - const invWorldScale = new THREE.Vector3( - 1 / this.worldScale.x, - 1 / this.worldScale.y, - 1 / this.worldScale.z, - ); - - this.plane.geometry.applyMatrix4( - new THREE.Matrix4().makeScale(invWorldScale.x, invWorldScale.y, invWorldScale.z), - ); } setDisplayCrosshair = (value: boolean): void => { @@ -171,8 +155,12 @@ class Plane { const localScaleFactorX = new THREE.Vector3(1, 0, 0); const localScaleFactorY = new THREE.Vector3(0, 1, 0); // Apply current rotation of the plane. - const localScaleFactorXRotated = localScaleFactorX.applyEuler(this.plane.rotation); - const localScaleFactorYRotated = localScaleFactorY.applyEuler(this.plane.rotation); + //const localScaleFactorXRotated = localScaleFactorX.applyEuler(this.container.rotation); + //const localScaleFactorYRotated = localScaleFactorY.applyEuler(this.container.rotation); + const quat = new THREE.Quaternion(); + this.plane.getWorldQuaternion(quat); + const localScaleFactorXRotated = localScaleFactorX.applyQuaternion(quat); + const localScaleFactorYRotated = localScaleFactorY.applyQuaternion(quat); // Put scale measuring vectors into world scale to get distortion cause by potentially anisotropic voxel scale factor. const scaleFactorXStrechedInSpace = localScaleFactorXRotated.multiply(this.worldScale); const scaleFactorYStrechedInSpace = localScaleFactorYRotated.multiply(this.worldScale); @@ -180,15 +168,19 @@ class Plane { const lengthX = scaleFactorXStrechedInSpace.length(); const lengthY = scaleFactorYStrechedInSpace.length(); // Calculate correction factors to neutralize the stretching caused by the scale factor. - const correctionX = lengthX / 1; - const correctionY = lengthY / 1; + const correctionX = lengthX; //!== 1 ? lengthX * 1.1 : lengthX; + const correctionY = lengthY; //!== 1 ? lengthY * 1.1 : lengthY; + //const correctionY = lengthY; const scaleVec = new THREE.Vector3().multiplyVectors( new THREE.Vector3(this.lastScaleFactors[0], this.lastScaleFactors[1], 1), - new THREE.Vector3(correctionX, correctionY, 1).multiply(this.worldScale), + new THREE.Vector3(correctionX, correctionY, 1), ); - if (this.planeID === "PLANE_XZ") console.log("calculated scale", this.planeID, scaleVec); + const scaleVec2 = new THREE.Vector3().multiplyVectors(scaleVec, this.worldScale); + if (this.planeID === "PLANE_XZ") + console.log("calculated scale", this.planeID, scaleVec, scaleVec2, this.worldScale); this.getMeshes().map((mesh) => mesh.scale.copy(scaleVec)); + //this.plane.scale.copy(scaleVec); } setBaseRotation = (rotVec: THREE.Euler): void => { From df0fdac2ec65604c3044c987ebd3da1d1d44d8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 9 May 2025 14:55:30 +0200 Subject: [PATCH 010/128] fix shearing & rotation for datasets with anisotropic scale --- .../oxalis/controller/scene_controller.ts | 4 +- .../javascripts/oxalis/geometries/plane.ts | 71 +++++++------------ 2 files changed, 29 insertions(+), 46 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 6a558a3d539..7284b9e1d71 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -130,12 +130,14 @@ class SceneController { this.scene = new THREE.Scene(); this.highlightedBBoxId = null; this.rootGroup = new THREE.Group(); + const planeMeshes = _.values(this.planes).flatMap((plane) => plane.getMeshes()); this.scene.add( this.rootGroup.add( this.rootNode, this.segmentMeshController.meshesLayerLODRootGroup, this.segmentMeshController.lightsGroup, ), + ...planeMeshes, ); // Because the voxel coordinates do not have a cube shape but are distorted, // we need to distort the entire scene to provide an illustration that is @@ -257,7 +259,6 @@ class SceneController { this.planes[OrthoViews.PLANE_YZ].setBaseRotation(OrthoBaseRotations[OrthoViews.PLANE_YZ]); this.planes[OrthoViews.PLANE_XZ].setBaseRotation(OrthoBaseRotations[OrthoViews.PLANE_XZ]); - const planeMeshes = _.values(this.planes).flatMap((plane) => plane.getMeshes()); this.rootNode = new THREE.Group().add( this.userBoundingBoxGroup, this.layerBoundingBoxGroup, @@ -268,7 +269,6 @@ class SceneController { ...this.areaMeasurementGeometry.getMeshes(), ), ...this.datasetBoundingBox.getMeshes(), - ...planeMeshes, ); if (state.annotation.skeleton != null) { diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index 4376ca15de4..042a688f8ba 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -7,6 +7,7 @@ import constants, { OrthoViewValues, } from "oxalis/constants"; import PlaneMaterialFactory from "oxalis/geometries/materials/plane_material_factory"; +import { listenToStoreProperty } from "oxalis/model/helpers/listener_helpers"; import { getBaseVoxelFactorsInUnit } from "oxalis/model/scaleinfo"; import Store from "oxalis/store"; import * as THREE from "three"; @@ -34,13 +35,15 @@ class Plane { planeID: OrthoView; materialFactory!: PlaneMaterialFactory; displayCrosshair: boolean; - worldScale: THREE.Vector3; + worldScaleInverse: THREE.Vector3; // @ts-expect-error ts-migrate(2564) FIXME: Property 'crosshair' has no initializer and is not... Remove this comment to see the full error message crosshair: Array; // @ts-expect-error ts-migrate(2564) FIXME: Property 'TDViewBorders' has no initializer and is... Remove this comment to see the full error message TDViewBorders: THREE.Line; lastScaleFactors: [number, number]; baseRotation: THREE.Euler; + storePropertyUnsubscribes: Array<() => void> = []; + datasetScaleFactor: THREE.Vector3 = new THREE.Vector3(1, 1, 1); constructor(planeID: OrthoView) { this.planeID = planeID; @@ -51,8 +54,9 @@ class Plane { // is smaller in voxels, so that it is squared in nm. // --> scaleInfo.baseVoxel const baseVoxelFactors = getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale); - this.worldScale = new THREE.Vector3(...baseVoxelFactors); + this.worldScaleInverse = new THREE.Vector3(...baseVoxelFactors); this.baseRotation = new THREE.Euler(0, 0, 0); + this.bindToEvents(); this.createMeshes(); } @@ -144,43 +148,11 @@ class Plane { if (this.lastScaleFactors[0] === xFactor && this.lastScaleFactors[1] === yFactor) { return; } - // TODOM: This is broken when rotation is active and needs to be fixed!!!! this.lastScaleFactors[0] = xFactor; this.lastScaleFactors[1] = yFactor; - this.recalculateScale(); - } - - private recalculateScale(): void { - // The baseScaleVector needs to be rotated like the current rotation settings of the plane to calculate the correct scaling vector. - const localScaleFactorX = new THREE.Vector3(1, 0, 0); - const localScaleFactorY = new THREE.Vector3(0, 1, 0); - // Apply current rotation of the plane. - //const localScaleFactorXRotated = localScaleFactorX.applyEuler(this.container.rotation); - //const localScaleFactorYRotated = localScaleFactorY.applyEuler(this.container.rotation); - const quat = new THREE.Quaternion(); - this.plane.getWorldQuaternion(quat); - const localScaleFactorXRotated = localScaleFactorX.applyQuaternion(quat); - const localScaleFactorYRotated = localScaleFactorY.applyQuaternion(quat); - // Put scale measuring vectors into world scale to get distortion cause by potentially anisotropic voxel scale factor. - const scaleFactorXStrechedInSpace = localScaleFactorXRotated.multiply(this.worldScale); - const scaleFactorYStrechedInSpace = localScaleFactorYRotated.multiply(this.worldScale); - // Measure visual length (how much stretch the voxel scale factor causes) - const lengthX = scaleFactorXStrechedInSpace.length(); - const lengthY = scaleFactorYStrechedInSpace.length(); - // Calculate correction factors to neutralize the stretching caused by the scale factor. - const correctionX = lengthX; //!== 1 ? lengthX * 1.1 : lengthX; - const correctionY = lengthY; //!== 1 ? lengthY * 1.1 : lengthY; - //const correctionY = lengthY; - - const scaleVec = new THREE.Vector3().multiplyVectors( - new THREE.Vector3(this.lastScaleFactors[0], this.lastScaleFactors[1], 1), - new THREE.Vector3(correctionX, correctionY, 1), - ); - const scaleVec2 = new THREE.Vector3().multiplyVectors(scaleVec, this.worldScale); - if (this.planeID === "PLANE_XZ") - console.log("calculated scale", this.planeID, scaleVec, scaleVec2, this.worldScale); - this.getMeshes().map((mesh) => mesh.scale.copy(scaleVec)); - //this.plane.scale.copy(scaleVec); + // Account for the dataset scale to match one world space coordinate to one dataset scale unit. + const scaleVector = new THREE.Vector3(xFactor, yFactor, 1).multiply(this.datasetScaleFactor); + this.getMeshes().map((mesh) => mesh.scale.copy(scaleVector)); } setBaseRotation = (rotVec: THREE.Euler): void => { @@ -192,7 +164,6 @@ class Plane { const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(rotVec); const combinedMatrix = rotationMatrix.multiply(baseRotationMatrix); this.getMeshes().map((mesh) => mesh.setRotationFromMatrix(combinedMatrix)); - this.recalculateScale(); }; // In case the plane's position was offset to make geometries @@ -200,14 +171,15 @@ class Plane { // additionally pass the originalPosition (which is necessary for the // shader) setPosition = (pos: Vector3, originalPosition?: Vector3): void => { - const [x, y, z] = pos; - this.TDViewBorders.position.set(x, y, z); - this.crosshair[0].position.set(x, y, z); - this.crosshair[1].position.set(x, y, z); - this.plane.position.set(x, y, z); + // TODOM: Write proper reasoning comment. + const scaledPosition = new THREE.Vector3(...pos).multiply(this.datasetScaleFactor); + this.TDViewBorders.position.set(scaledPosition.x, scaledPosition.y, scaledPosition.z); + this.crosshair[0].position.set(scaledPosition.x, scaledPosition.y, scaledPosition.z); + this.crosshair[1].position.set(scaledPosition.x, scaledPosition.y, scaledPosition.z); + this.plane.position.set(scaledPosition.x, scaledPosition.y, scaledPosition.z); if (originalPosition == null) { - this.plane.material.setGlobalPosition(x, y, z); + this.plane.material.setGlobalPosition(scaledPosition.x, scaledPosition.y, scaledPosition.z); } else { this.plane.material.setGlobalPosition( originalPosition[0], @@ -231,6 +203,17 @@ class Plane { destroy() { this.materialFactory.destroy(); + this.storePropertyUnsubscribes.forEach((f) => f()); + this.storePropertyUnsubscribes = []; + } + + bindToEvents(): void { + this.storePropertyUnsubscribes = [ + listenToStoreProperty( + (storeState) => storeState.dataset.dataSource.scale.factor, + (scaleFactor) => (this.datasetScaleFactor = new THREE.Vector3(...scaleFactor)), + ), + ]; } } From 9109d0770bdd8229202fe1bcd3d87b0f43f2fdcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 9 May 2025 16:23:09 +0200 Subject: [PATCH 011/128] clean up scene graph hierarchy --- .../oxalis/controller/scene_controller.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 7284b9e1d71..f86570c4f23 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -130,14 +130,12 @@ class SceneController { this.scene = new THREE.Scene(); this.highlightedBBoxId = null; this.rootGroup = new THREE.Group(); - const planeMeshes = _.values(this.planes).flatMap((plane) => plane.getMeshes()); this.scene.add( this.rootGroup.add( this.rootNode, this.segmentMeshController.meshesLayerLODRootGroup, this.segmentMeshController.lightsGroup, ), - ...planeMeshes, ); // Because the voxel coordinates do not have a cube shape but are distorted, // we need to distort the entire scene to provide an illustration that is @@ -259,6 +257,16 @@ class SceneController { this.planes[OrthoViews.PLANE_YZ].setBaseRotation(OrthoBaseRotations[OrthoViews.PLANE_YZ]); this.planes[OrthoViews.PLANE_XZ].setBaseRotation(OrthoBaseRotations[OrthoViews.PLANE_XZ]); + const planeGroup = new THREE.Group(); + _.values(this.planes).forEach((plane) => planeGroup.add(...plane.getMeshes())); + // Apply the inverse dataset scale factor to all planes to remove the scaling of the root group + // to avoid shearing effects on rotated ortho viewport planes. For more info see plane.ts. + planeGroup.scale.copy( + new THREE.Vector3(1, 1, 1).divide( + new THREE.Vector3(...Store.getState().dataset.dataSource.scale.factor), + ), + ); + this.rootNode = new THREE.Group().add( this.userBoundingBoxGroup, this.layerBoundingBoxGroup, @@ -269,6 +277,7 @@ class SceneController { ...this.areaMeasurementGeometry.getMeshes(), ), ...this.datasetBoundingBox.getMeshes(), + planeGroup, ); if (state.annotation.skeleton != null) { From 5eb8f249163fb6cc1a440d31cebd75915cdcac42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 9 May 2025 16:42:49 +0200 Subject: [PATCH 012/128] small potential speedup avoiding object creation --- frontend/javascripts/libs/mjs.ts | 4 +++ .../javascripts/oxalis/geometries/plane.ts | 25 +++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/frontend/javascripts/libs/mjs.ts b/frontend/javascripts/libs/mjs.ts index f1e825a5ea5..dc6b4bf1a54 100644 --- a/frontend/javascripts/libs/mjs.ts +++ b/frontend/javascripts/libs/mjs.ts @@ -387,6 +387,10 @@ const V3 = { prod(a: Vector3) { return a[0] * a[1] * a[2]; }, + + multiply(a: Vector3, b: Vector3): Vector3 { + return [a[0] * b[0], a[1] * b[1], a[2] * b[2]]; + }, }; const V4 = { diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index 042a688f8ba..23bc87f6e64 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -1,3 +1,4 @@ +import { V3 } from "libs/mjs"; import _ from "lodash"; import type { OrthoView, Vector3 } from "oxalis/constants"; import constants, { @@ -43,7 +44,7 @@ class Plane { lastScaleFactors: [number, number]; baseRotation: THREE.Euler; storePropertyUnsubscribes: Array<() => void> = []; - datasetScaleFactor: THREE.Vector3 = new THREE.Vector3(1, 1, 1); + datasetScaleFactor: Vector3 = [1, 1, 1]; constructor(planeID: OrthoView) { this.planeID = planeID; @@ -151,8 +152,12 @@ class Plane { this.lastScaleFactors[0] = xFactor; this.lastScaleFactors[1] = yFactor; // Account for the dataset scale to match one world space coordinate to one dataset scale unit. - const scaleVector = new THREE.Vector3(xFactor, yFactor, 1).multiply(this.datasetScaleFactor); - this.getMeshes().map((mesh) => mesh.scale.copy(scaleVector)); + const scaleVector: Vector3 = [ + xFactor * this.datasetScaleFactor[0], + yFactor * this.datasetScaleFactor[1], + 1 * this.datasetScaleFactor[2], + ]; + this.getMeshes().map((mesh) => mesh.scale.set(...scaleVector)); } setBaseRotation = (rotVec: THREE.Euler): void => { @@ -172,14 +177,14 @@ class Plane { // shader) setPosition = (pos: Vector3, originalPosition?: Vector3): void => { // TODOM: Write proper reasoning comment. - const scaledPosition = new THREE.Vector3(...pos).multiply(this.datasetScaleFactor); - this.TDViewBorders.position.set(scaledPosition.x, scaledPosition.y, scaledPosition.z); - this.crosshair[0].position.set(scaledPosition.x, scaledPosition.y, scaledPosition.z); - this.crosshair[1].position.set(scaledPosition.x, scaledPosition.y, scaledPosition.z); - this.plane.position.set(scaledPosition.x, scaledPosition.y, scaledPosition.z); + const scaledPosition = V3.multiply(pos, this.datasetScaleFactor); + this.TDViewBorders.position.set(...scaledPosition); + this.crosshair[0].position.set(...scaledPosition); + this.crosshair[1].position.set(...scaledPosition); + this.plane.position.set(...scaledPosition); if (originalPosition == null) { - this.plane.material.setGlobalPosition(scaledPosition.x, scaledPosition.y, scaledPosition.z); + this.plane.material.setGlobalPosition(...scaledPosition); } else { this.plane.material.setGlobalPosition( originalPosition[0], @@ -211,7 +216,7 @@ class Plane { this.storePropertyUnsubscribes = [ listenToStoreProperty( (storeState) => storeState.dataset.dataSource.scale.factor, - (scaleFactor) => (this.datasetScaleFactor = new THREE.Vector3(...scaleFactor)), + (scaleFactor) => (this.datasetScaleFactor = scaleFactor), ), ]; } From 639e7e61d81272b77d6515ec47e8d1310fdc95ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 9 May 2025 17:10:44 +0200 Subject: [PATCH 013/128] remove doublesided material setting for plane as not needed --- frontend/javascripts/oxalis/geometries/plane.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index 23bc87f6e64..df9712f0c3a 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -74,7 +74,6 @@ class Plane { const textureMaterial = this.materialFactory.setup().getMaterial(); this.plane = new THREE.Mesh(planeGeo, textureMaterial); this.plane.name = `${this.planeID}-plane`; - this.plane.material.side = THREE.DoubleSide; // Create crosshairs this.crosshair = new Array(2); From 1d5d4993261e832b835c36fbb0ec89d87ff8996f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 9 May 2025 18:24:59 +0200 Subject: [PATCH 014/128] fix move tool in arbitrary orientation --- frontend/javascripts/oxalis/constants.ts | 9 ++++++ .../oxalis/controller/camera_controller.ts | 3 +- .../oxalis/controller/scene_controller.ts | 8 +---- .../oxalis/model/reducers/flycam_reducer.ts | 29 ++++++++++++------- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/frontend/javascripts/oxalis/constants.ts b/frontend/javascripts/oxalis/constants.ts index 3d341a73337..ca4e2dd8eeb 100644 --- a/frontend/javascripts/oxalis/constants.ts +++ b/frontend/javascripts/oxalis/constants.ts @@ -1,4 +1,5 @@ import type { AdditionalCoordinate } from "types/api_types"; +import * as THREE from "three"; export const ViewModeValues = ["orthogonal", "flight", "oblique"] as ViewMode[]; @@ -116,6 +117,14 @@ export const OrthoViewCrosshairColors: OrthoViewMap<[number, number]> = { [OrthoViews.PLANE_XZ]: [BLUE, PINK], [OrthoViews.TDView]: [0x000000, 0x000000], }; +export const OrthoBaseRotations = { + //[OrthoViews.PLANE_XY]: new THREE.Euler(Math.PI, 0, 0), + [OrthoViews.PLANE_XY]: new THREE.Euler(Math.PI, 0, 0), + [OrthoViews.PLANE_YZ]: new THREE.Euler(Math.PI, (1 / 2) * Math.PI, 0), + [OrthoViews.PLANE_XZ]: new THREE.Euler((-1 / 2) * Math.PI, 0, 0), + [OrthoViews.TDView]: new THREE.Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4), +}; + export type BorderTabType = { id: string; name: string; diff --git a/frontend/javascripts/oxalis/controller/camera_controller.ts b/frontend/javascripts/oxalis/controller/camera_controller.ts index 227c11af652..f082c61a8c3 100644 --- a/frontend/javascripts/oxalis/controller/camera_controller.ts +++ b/frontend/javascripts/oxalis/controller/camera_controller.ts @@ -2,7 +2,7 @@ import { V3 } from "libs/mjs"; import * as Utils from "libs/utils"; import _ from "lodash"; import type { OrthoView, OrthoViewMap, OrthoViewRects, Vector3 } from "oxalis/constants"; -import { OrthoViewValuesWithoutTDView, OrthoViews } from "oxalis/constants"; +import { OrthoBaseRotations, OrthoViewValuesWithoutTDView, OrthoViews } from "oxalis/constants"; import { getDatasetCenter, getDatasetExtentInUnit } from "oxalis/model/accessors/dataset_accessor"; import { getPosition, getRotationInRadian } from "oxalis/model/accessors/flycam_accessor"; import { @@ -18,7 +18,6 @@ import Store from "oxalis/store"; import * as React from "react"; import * as THREE from "three"; import TWEEN from "tween.js"; -import { OrthoBaseRotations } from "./scene_controller"; type Props = { cameras: OrthoViewMap; diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index f86570c4f23..6519f7e6782 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -13,6 +13,7 @@ import type { Vector3, } from "oxalis/constants"; import constants, { + OrthoBaseRotations, OrthoViews, OrthoViewValuesWithoutTDView, TDViewDisplayModeEnum, @@ -73,13 +74,6 @@ THREE.Mesh.prototype.raycast = acceleratedRaycast; const CUBE_COLOR = 0x999999; const LAYER_CUBE_COLOR = 0xffff99; -export const OrthoBaseRotations = { - [OrthoViews.PLANE_XY]: new THREE.Euler(Math.PI, 0, 0), - [OrthoViews.PLANE_YZ]: new THREE.Euler(Math.PI, (1 / 2) * Math.PI, 0), - [OrthoViews.PLANE_XZ]: new THREE.Euler((-1 / 2) * Math.PI, 0, 0), - [OrthoViews.TDView]: new THREE.Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4), -}; - const getVisibleSegmentationLayerNames = reuseInstanceOnEquality((storeState: WebknossosState) => getVisibleSegmentationLayers(storeState).map((l) => l.name), ); diff --git a/frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts b/frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts index 96048cc01f1..bd08727789b 100644 --- a/frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/flycam_reducer.ts @@ -6,6 +6,7 @@ import _ from "lodash"; import type { Vector3 } from "oxalis/constants"; import { ZOOM_STEP_INTERVAL, + getRotationInRadian, getValidZoomRangeForUser, } from "oxalis/model/accessors/flycam_accessor"; import type { Action } from "oxalis/model/actions/actions"; @@ -13,6 +14,7 @@ import Dimensions from "oxalis/model/dimensions"; import { getBaseVoxelFactorsInUnit } from "oxalis/model/scaleinfo"; import type { WebknossosState } from "oxalis/store"; import { getUnifiedAdditionalCoordinates } from "../accessors/dataset_accessor"; +import * as THREE from "three"; function cloneMatrix(m: Matrix4x4): Matrix4x4 { return [ @@ -275,7 +277,7 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState } case "SET_ROTATION": { - return setRotationReducer(state, action.rotation); + return setRotationReducer(state, action.rotation); } case "SET_DIRECTION": { @@ -314,26 +316,31 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState } case "MOVE_PLANE_FLYCAM_ORTHO": { - const { dataset } = state; + const { dataset, flycam } = state; if (dataset != null) { const { planeId, increaseSpeedWithZoom } = action; const vector = Dimensions.transDim(action.vector, planeId); - const zoomFactor = increaseSpeedWithZoom ? state.flycam.zoomStep : 1; + const flycamRotation = getRotationInRadian(flycam); + + const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler( + new THREE.Euler(...flycamRotation), + ); + const movementVectorInWorld = new THREE.Vector3(...vector).applyMatrix4(rotationMatrix); + const zoomFactor = increaseSpeedWithZoom ? flycam.zoomStep : 1; const scaleFactor = getBaseVoxelFactorsInUnit(dataset.dataSource.scale); - const delta: Vector3 = [ - vector[0] * zoomFactor * scaleFactor[0], - vector[1] * zoomFactor * scaleFactor[1], - vector[2] * zoomFactor * scaleFactor[2], - ]; + const movementInWorldZoomed = movementVectorInWorld + .multiplyScalar(zoomFactor) + .multiply(new THREE.Vector3(...scaleFactor)); + // TODOM: make this apply to movementInWorldZoomed if (planeId != null && state.userConfiguration.dynamicSpaceDirection) { // change direction of the value connected to space, based on the last direction - const dim = Dimensions.getIndices(planeId)[2]; - delta[dim] *= state.flycam.spaceDirectionOrtho[dim]; + // const dim = Dimensions.getIndices(planeId)[2]; + // delta[dim] *= state.flycam.spaceDirectionOrtho[dim]; } - return moveReducer(state, delta); + return moveReducer(state, movementInWorldZoomed.toArray()); } return state; From d6557d85c5fadc88d82f11e7b1400c76ddabe846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 12 May 2025 17:59:27 +0200 Subject: [PATCH 015/128] fix line measurement tool location & rendering --- .../controller/combinations/tool_controls.ts | 8 +- .../oxalis/controller/scene_controller.ts | 14 ++- frontend/javascripts/oxalis/default_state.ts | 1 + .../javascripts/oxalis/geometries/plane.ts | 11 +-- .../model/accessors/view_mode_accessor.ts | 95 ++++++------------- .../oxalis/model/actions/ui_actions.ts | 12 ++- .../oxalis/model/reducers/ui_reducer.ts | 3 +- frontend/javascripts/oxalis/store.ts | 7 +- .../view/distance_measurement_tooltip.tsx | 58 +++++------ 9 files changed, 95 insertions(+), 114 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts b/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts index 25f5c5fa516..0a37b436f3a 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts @@ -48,7 +48,7 @@ import { hideMeasurementTooltipAction, setActiveUserBoundingBoxId, setIsMeasuringAction, - setLastMeasuredPositionAction, + setLastMeasuredAndViewportPositionAction, setQuickSelectStateAction, } from "oxalis/model/actions/ui_actions"; import { @@ -897,7 +897,7 @@ export class LineMeasurementToolController { const state = Store.getState(); const newPos = V3.floor(calculateGlobalPos(state, pos, this.initialPlane)); lineMeasurementGeometry.updateLatestPointPosition(newPos); - Store.dispatch(setLastMeasuredPositionAction(newPos)); + Store.dispatch(setLastMeasuredAndViewportPositionAction(newPos, pos)); }; const rightClick = (pos: Point2, plane: OrthoView, event: MouseEvent) => { // In case the tool was reset by the user, abort measuring. @@ -946,7 +946,7 @@ export class LineMeasurementToolController { } else { lineMeasurementGeometry.addPoint(position); } - Store.dispatch(setLastMeasuredPositionAction(position)); + Store.dispatch(setLastMeasuredAndViewportPositionAction(position, pos)); }; return { mouseMove, @@ -1017,7 +1017,7 @@ export class AreaMeasurementToolController { const state = Store.getState(); const position = V3.floor(calculateGlobalPos(state, pos, this.initialPlane)); areaMeasurementGeometry.addEdgePoint(position); - Store.dispatch(setLastMeasuredPositionAction(position)); + Store.dispatch(setLastMeasuredAndViewportPositionAction(position, pos)); }, leftMouseUp: () => { if (!this.isMeasuring) { diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 6519f7e6782..df3e7bb6796 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -427,11 +427,17 @@ class SceneController { const ind = Dimensions.getIndices(planeId); // Offset the plane so the user can see the skeletonTracing behind the plane - // TODOM: This concept is kinda broken when rotations are turned on. TODO investigate how to implement this instead. - //pos[ind[2]] += planeId === OrthoViews.PLANE_XY ? 1 : -1; - //pos[ind[2]] += + // TODOM: Somehow data is rendered slightly besides the borders due to this displacement. TODO: Investigate why + // pos[ind[2]] += // planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; - this.planes[planeId].setPosition(pos, originalPosition); + const unrotatedThirdDimOfPlane = [0, 0, 0]; + unrotatedThirdDimOfPlane[ind[2]] = + planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; + const rotatedThirdDimOfPlane = new THREE.Vector3(...unrotatedThirdDimOfPlane).applyEuler( + new THREE.Euler(...rotation), + ); + const positionWithOffset = V3.add(pos, rotatedThirdDimOfPlane.toArray()); + this.planes[planeId].setPosition(positionWithOffset, originalPosition); this.planes[planeId].setRotation(new THREE.Euler(rotation[0], rotation[1], rotation[2])); this.quickSelectGeometry.adaptVisibilityForRendering(originalPosition, ind[2]); diff --git a/frontend/javascripts/oxalis/default_state.ts b/frontend/javascripts/oxalis/default_state.ts index c2a5f311541..f31025601f7 100644 --- a/frontend/javascripts/oxalis/default_state.ts +++ b/frontend/javascripts/oxalis/default_state.ts @@ -259,6 +259,7 @@ const defaultState: WebknossosState = { measurementToolInfo: { lastMeasuredPosition: null, isMeasuring: false, + viewportPosition: null, }, navbarHeight: constants.DEFAULT_NAVBAR_HEIGHT, contextInfo: { diff --git a/frontend/javascripts/oxalis/geometries/plane.ts b/frontend/javascripts/oxalis/geometries/plane.ts index df9712f0c3a..ca830e002c8 100644 --- a/frontend/javascripts/oxalis/geometries/plane.ts +++ b/frontend/javascripts/oxalis/geometries/plane.ts @@ -182,15 +182,8 @@ class Plane { this.crosshair[1].position.set(...scaledPosition); this.plane.position.set(...scaledPosition); - if (originalPosition == null) { - this.plane.material.setGlobalPosition(...scaledPosition); - } else { - this.plane.material.setGlobalPosition( - originalPosition[0], - originalPosition[1], - originalPosition[2], - ); - } + const scaledOriginalPosition = V3.multiply(originalPosition || pos, this.datasetScaleFactor); + this.plane.material.setGlobalPosition(...scaledOriginalPosition); }; setVisible = (isVisible: boolean, isDataVisible?: boolean): void => { diff --git a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.ts b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.ts index 9cbaaf1e674..e4b745557db 100644 --- a/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/view_mode_accessor.ts @@ -17,9 +17,11 @@ import constants, { OrthoViewValuesWithoutTDView, } from "oxalis/constants"; import { reuseInstanceOnEquality } from "oxalis/model/accessors/accessor_helpers"; -import { getPosition } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInRadian } from "oxalis/model/accessors/flycam_accessor"; import { getBaseVoxelFactorsInUnit } from "oxalis/model/scaleinfo"; import type { Flycam, WebknossosState } from "oxalis/store"; +import Dimensions from "../dimensions"; +import * as THREE from "three"; export function getTDViewportSize(state: WebknossosState): [number, number] { const camera = state.viewModeData.plane.tdCamera; @@ -98,37 +100,54 @@ function _calculateMaybeGlobalPos( let position: Vector3; planeId = planeId || state.viewModeData.plane.activeViewport; const curGlobalPos = getPosition(state.flycam); + const flycamRotation = getRotationInRadian(state.flycam); const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); const { width, height } = getInputCatcherRect(state, planeId); // Subtract clickPos from only half of the viewport extent as // the center of the viewport / the flycam position is used as a reference point. - const diffX = (width / 2 - clickPos.x) * state.flycam.zoomStep; - const diffY = (height / 2 - clickPos.y) * state.flycam.zoomStep; + const diffX = clickPos.x === 0 ? 0 : (width / 2 - clickPos.x) * state.flycam.zoomStep; + const diffY = clickPos.y === 0 ? 0 : (height / 2 - clickPos.y) * state.flycam.zoomStep; + const positionInPlane = [diffX, diffY, 0] as Vector3; + const positionPlaneDefaultRotation = Dimensions.transDim(positionInPlane, planeId); + const flycamRotationMatrix = new THREE.Matrix4().makeRotationFromEuler( + new THREE.Euler(...flycamRotation), + ); + const flycamPositionMatrix = new THREE.Matrix4().makeTranslation( + new THREE.Vector3(...curGlobalPos), + ); + const rotatedPosition = new THREE.Vector3(...positionPlaneDefaultRotation).applyMatrix4( + flycamRotationMatrix, + ); + const scaledRotatedPosition = rotatedPosition + .multiply(new THREE.Vector3(...planeRatio)) + .multiplyScalar(-1); + + const globalFloatingPosition = scaledRotatedPosition.applyMatrix4(flycamPositionMatrix); switch (planeId) { case OrthoViews.PLANE_XY: { position = [ - Math.round(curGlobalPos[0] - diffX * planeRatio[0]), - Math.round(curGlobalPos[1] - diffY * planeRatio[1]), - Math.floor(curGlobalPos[2]), + Math.round(globalFloatingPosition.x), + Math.round(globalFloatingPosition.y), + Math.floor(globalFloatingPosition.z), ]; break; } case OrthoViews.PLANE_YZ: { position = [ - Math.floor(curGlobalPos[0]), - Math.round(curGlobalPos[1] - diffY * planeRatio[1]), - Math.round(curGlobalPos[2] - diffX * planeRatio[2]), + Math.floor(globalFloatingPosition.x), + Math.round(globalFloatingPosition.y), + Math.round(globalFloatingPosition.z), ]; break; } case OrthoViews.PLANE_XZ: { position = [ - Math.round(curGlobalPos[0] - diffX * planeRatio[0]), - Math.floor(curGlobalPos[1]), - Math.round(curGlobalPos[2] - diffY * planeRatio[2]), + Math.round(globalFloatingPosition.x), + Math.floor(globalFloatingPosition.y), + Math.round(globalFloatingPosition.z), ]; break; } @@ -140,57 +159,6 @@ function _calculateMaybeGlobalPos( return position; } -function _calculateMaybePlaneScreenPos( - state: WebknossosState, - globalPosition: Vector3, - planeId?: OrthoView | null | undefined, -): Point2 | null | undefined { - // This method does the reverse of _calculateMaybeGlobalPos. It takes a global position - // and calculates the corresponding screen position in the given plane. - // This is achieved by reversing the calculations in _calculateMaybeGlobalPos. - let point: Point2; - planeId = planeId || state.viewModeData.plane.activeViewport; - const navbarHeight = state.uiInformation.navbarHeight; - const curGlobalPos = getPosition(state.flycam); - const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); - const { width, height, top, left } = getInputCatcherRect(state, planeId); - const positionDiff = V3.sub(globalPosition, curGlobalPos); - switch (planeId) { - case OrthoViews.PLANE_XY: { - point = { - x: positionDiff[0] / state.flycam.zoomStep / planeRatio[0], - y: positionDiff[1] / state.flycam.zoomStep / planeRatio[1], - }; - break; - } - - case OrthoViews.PLANE_YZ: { - point = { - x: positionDiff[2] / state.flycam.zoomStep / planeRatio[2], - y: positionDiff[1] / state.flycam.zoomStep / planeRatio[1], - }; - break; - } - - case OrthoViews.PLANE_XZ: { - point = { - x: positionDiff[0] / state.flycam.zoomStep / planeRatio[0], - y: positionDiff[2] / state.flycam.zoomStep / planeRatio[2], - }; - break; - } - - default: - return null; - } - point.x += width / 2 + left; - point.y += height / 2 + top + navbarHeight; - point.x = Math.round(point.x); - point.y = Math.round(point.y); - - return point; -} - function _calculateMaybeGlobalDelta( state: WebknossosState, delta: Point2, @@ -288,7 +256,6 @@ export function getDisplayedDataExtentInPlaneMode(state: WebknossosState) { export const calculateMaybeGlobalPos = reuseInstanceOnEquality(_calculateMaybeGlobalPos); export const calculateGlobalPos = reuseInstanceOnEquality(_calculateGlobalPos); export const calculateGlobalDelta = reuseInstanceOnEquality(_calculateGlobalDelta); -export const calculateMaybePlaneScreenPos = reuseInstanceOnEquality(_calculateMaybePlaneScreenPos); export function getViewMode(state: WebknossosState): ViewMode { return state.temporaryConfiguration.viewMode; } diff --git a/frontend/javascripts/oxalis/model/actions/ui_actions.ts b/frontend/javascripts/oxalis/model/actions/ui_actions.ts index a2cf8c14481..bd1d9375788 100644 --- a/frontend/javascripts/oxalis/model/actions/ui_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/ui_actions.ts @@ -1,4 +1,4 @@ -import type { OrthoView, Vector3 } from "oxalis/constants"; +import type { OrthoView, Point2, Vector3 } from "oxalis/constants"; import type { AnnotationTool } from "oxalis/model/accessors/tool_accessor"; import type { BorderOpenStatus, Theme, WebknossosState } from "oxalis/store"; import type { StartAIJobModalState } from "oxalis/view/action-bar/starting_job_modals"; @@ -24,7 +24,7 @@ export type EscapeAction = ReturnType; export type SetQuickSelectStateAction = ReturnType; type ShowQuickSelectSettingsAction = ReturnType; type HideMeasurementTooltipAction = ReturnType; -type SetLastMeasuredPositionAction = ReturnType; +type SetLastMeasuredPositionAction = ReturnType; type SetIsMeasuringAction = ReturnType; type SetNavbarHeightAction = ReturnType; type ShowContextMenuAction = ReturnType; @@ -201,10 +201,14 @@ export const hideMeasurementTooltipAction = () => ({ type: "HIDE_MEASUREMENT_TOOLTIP", }) as const; -export const setLastMeasuredPositionAction = (position: Vector3) => +export const setLastMeasuredAndViewportPositionAction = ( + lastMeasuredPosition: Vector3, + viewportPosition: Point2, +) => ({ type: "SET_LAST_MEASURED_POSITION", - position, + lastMeasuredPosition, + viewportPosition, }) as const; export const setIsMeasuringAction = (isMeasuring: boolean) => ({ diff --git a/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts b/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts index b8e3258e00d..281569b8b6a 100644 --- a/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts @@ -178,7 +178,8 @@ function UiReducer(state: WebknossosState, action: Action): WebknossosState { case "SET_LAST_MEASURED_POSITION": { return updateKey2(state, "uiInformation", "measurementToolInfo", { - lastMeasuredPosition: action.position, + lastMeasuredPosition: action.lastMeasuredPosition, + viewportPosition: action.viewportPosition, }); } case "SET_IS_MEASURING": { diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index d923c53761e..30b9c0d36dd 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -10,6 +10,7 @@ import type { OrthoView, OrthoViewWithoutTD, OverwriteMode, + Point2, Rect, TDViewDisplayMode, TreeType, @@ -567,7 +568,11 @@ type UiInformation = { | "drawing" // the user is currently drawing a bounding box | "active"; // the quick select saga is currently running (calculating as well as preview mode) readonly areQuickSelectSettingsOpen: boolean; - readonly measurementToolInfo: { lastMeasuredPosition: Vector3 | null; isMeasuring: boolean }; + readonly measurementToolInfo: { + lastMeasuredPosition: Vector3 | null; + isMeasuring: boolean; + viewportPosition: Point2 | null; + }; readonly navbarHeight: number; readonly contextInfo: { readonly contextMenuPosition: Readonly<[number, number]> | null | undefined; diff --git a/frontend/javascripts/oxalis/view/distance_measurement_tooltip.tsx b/frontend/javascripts/oxalis/view/distance_measurement_tooltip.tsx index bed2eaf9160..a62b7b33533 100644 --- a/frontend/javascripts/oxalis/view/distance_measurement_tooltip.tsx +++ b/frontend/javascripts/oxalis/view/distance_measurement_tooltip.tsx @@ -9,14 +9,12 @@ import { } from "libs/format_utils"; import { useWkSelector } from "libs/react_hooks"; import { clamp } from "libs/utils"; +import _ from "lodash"; import { LongUnitToShortUnitMap, type Vector3 } from "oxalis/constants"; import getSceneController from "oxalis/controller/scene_controller_provider"; -import { getPosition } from "oxalis/model/accessors/flycam_accessor"; +import { getPosition, getRotationInDegrees } from "oxalis/model/accessors/flycam_accessor"; import { AnnotationTool, MeasurementTools } from "oxalis/model/accessors/tool_accessor"; -import { - calculateMaybePlaneScreenPos, - getInputCatcherRect, -} from "oxalis/model/accessors/view_mode_accessor"; +import { getInputCatcherRect } from "oxalis/model/accessors/view_mode_accessor"; import { hideMeasurementTooltipAction } from "oxalis/model/actions/ui_actions"; import dimensions from "oxalis/model/dimensions"; import { useEffect, useRef } from "react"; @@ -41,9 +39,12 @@ function DistanceEntry({ distance }: { distance: string }) { } export default function DistanceMeasurementTooltip() { - const position = useWkSelector( + const lastMeasuredGlobalPosition = useWkSelector( (state) => state.uiInformation.measurementToolInfo.lastMeasuredPosition, ); + const lastMeasuredViewportPosition = useWkSelector( + (state) => state.uiInformation.measurementToolInfo.viewportPosition, + ); const isMeasuring = useWkSelector((state) => state.uiInformation.measurementToolInfo.isMeasuring); const flycam = useWkSelector((state) => state.flycam); const state = useWkSelector((state) => state); @@ -52,6 +53,7 @@ export default function DistanceMeasurementTooltip() { const tooltipRef = useRef(null); const dispatch = useDispatch(); const currentPosition = getPosition(flycam); + const rotation = getRotationInDegrees(flycam); const { areaMeasurementGeometry, lineMeasurementGeometry } = getSceneController(); const activeGeometry = activeTool === AnnotationTool.LINE_MEASUREMENT @@ -62,22 +64,27 @@ export default function DistanceMeasurementTooltip() { const thirdDim = dimensions.thirdDimensionForPlane(orthoView); // biome-ignore lint/correctness/useExhaustiveDependencies(thirdDim): thirdDim is more or less a constant - // biome-ignore lint/correctness/useExhaustiveDependencies(position[thirdDim]): + // biome-ignore lint/correctness/useExhaustiveDependencies(lastMeasuredGlobalPosition[thirdDim]): // biome-ignore lint/correctness/useExhaustiveDependencies(hideMeasurementTooltipAction): constant // biome-ignore lint/correctness/useExhaustiveDependencies(dispatch): constant - // biome-ignore lint/correctness/useExhaustiveDependencies(position): + // biome-ignore lint/correctness/useExhaustiveDependencies(lastMeasuredGlobalPosition): // biome-ignore lint/correctness/useExhaustiveDependencies(activeGeometry.resetAndHide): useEffect(() => { if ( - position != null && - Math.floor(currentPosition[thirdDim]) !== Math.floor(position[thirdDim]) + lastMeasuredGlobalPosition != null && + _.isEqual(rotation, [0, 0, 0]) && // TODOM: Improve this check to remove the tooltip once moved back / forth regardless of what the current rotation is. + Math.floor(currentPosition[thirdDim]) !== Math.floor(lastMeasuredGlobalPosition[thirdDim]) ) { dispatch(hideMeasurementTooltipAction()); activeGeometry.resetAndHide(); } - }, [currentPosition[thirdDim]]); + }, [currentPosition[thirdDim], rotation]); - if (position == null || !MeasurementTools.includes(activeTool)) { + if ( + lastMeasuredGlobalPosition == null || + lastMeasuredViewportPosition == null || + !MeasurementTools.includes(activeTool) + ) { return null; } @@ -107,23 +114,20 @@ export default function DistanceMeasurementTooltip() { width: viewportWidth, height: viewportHeight, } = getInputCatcherRect(state, orthoView); - const tooltipPosition = calculateMaybePlaneScreenPos(state, position, orthoView); - - if (tooltipPosition == null) { - return null; - } const tooltipWidth = tooltipRef.current?.offsetWidth ?? 0; - const left = clamp( - viewportLeft + ADDITIONAL_OFFSET - tooltipWidth, - tooltipPosition.x + ADDITIONAL_OFFSET, - viewportLeft + viewportWidth - ADDITIONAL_OFFSET, - ); - const top = clamp( - viewportTop + ADDITIONAL_OFFSET, - tooltipPosition.y - TOOLTIP_HEIGHT - ADDITIONAL_OFFSET, - viewportTop + viewportHeight + TOOLTIP_HEIGHT - ADDITIONAL_OFFSET, - ); + const left = + clamp( + ADDITIONAL_OFFSET - tooltipWidth, + lastMeasuredViewportPosition.x + ADDITIONAL_OFFSET, + viewportWidth - ADDITIONAL_OFFSET, + ) + viewportLeft; + const top = + clamp( + ADDITIONAL_OFFSET, + lastMeasuredViewportPosition.y - ADDITIONAL_OFFSET, + viewportHeight + TOOLTIP_HEIGHT - ADDITIONAL_OFFSET, + ) + viewportTop; return (
Date: Mon, 12 May 2025 18:11:39 +0200 Subject: [PATCH 016/128] fix f-ing through dataset --- .../oxalis/controller/combinations/move_handlers.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/move_handlers.ts b/frontend/javascripts/oxalis/controller/combinations/move_handlers.ts index ae0bef4ed85..8b221b2b4b6 100644 --- a/frontend/javascripts/oxalis/controller/combinations/move_handlers.ts +++ b/frontend/javascripts/oxalis/controller/combinations/move_handlers.ts @@ -53,9 +53,10 @@ export const moveW = (deltaW: number, oneSlide: boolean): void => { const wDim = Dimensions.getIndices(activeViewport)[2]; const wStep = (representativeMag || [1, 1, 1])[wDim]; Store.dispatch( - moveFlycamOrthoAction( - Dimensions.transDim([0, 0, Math.sign(deltaW) * Math.max(1, wStep)], activeViewport), + movePlaneFlycamOrthoAction( + [0, 0, Math.sign(deltaW) * Math.max(1, wStep)], activeViewport, + false, ), ); } else { From 4f5967c8c743e64415e9bcefd20d7e53c5462899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 12 May 2025 19:25:46 +0200 Subject: [PATCH 017/128] disable unsupported tools when rotated in first version --- .../model/accessors/disabled_tool_accessor.ts | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/oxalis/model/accessors/disabled_tool_accessor.ts index 29cd828b1e6..b3a9b3d9dad 100644 --- a/frontend/javascripts/oxalis/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/disabled_tool_accessor.ts @@ -6,7 +6,10 @@ import { import memoizeOne from "memoize-one"; import { IdentityTransform } from "oxalis/constants"; import { getVisibleSegmentationLayer } from "oxalis/model/accessors/dataset_accessor"; -import { isMagRestrictionViolated } from "oxalis/model/accessors/flycam_accessor"; +import { + getRotationInRadian, + isMagRestrictionViolated, +} from "oxalis/model/accessors/flycam_accessor"; import type { WebknossosState } from "oxalis/store"; import type { APIOrganization, APIUser } from "types/api_types"; import { reuseInstanceOnEquality } from "./accessor_helpers"; @@ -21,6 +24,7 @@ import { isVolumeAnnotationDisallowedForZoom, } from "oxalis/model/accessors/volumetracing_accessor"; import { AnnotationTool, type AnnotationToolId } from "./tool_accessor"; +import _ from "lodash"; type DisabledInfo = { isDisabled: boolean; @@ -41,6 +45,9 @@ const noSkeletonsExplanation = const disabledSkeletonExplanation = "Currently all trees are invisible. To use this tool, make the skeleton layer visible by toggling the button in the left sidebar."; +const rotationActiveDisabledExplanation = + "The tools is disabled because you are currently viewing the dataset rotated. Please reset the rotation to [0,0,0] to be able to use this tool."; + const getExplanationForDisabledVolume = ( isSegmentationTracingVisible: boolean, isInMergerMode: boolean, @@ -49,11 +56,16 @@ const getExplanationForDisabledVolume = ( isEditableMappingActive: boolean, isSegmentationTracingTransformed: boolean, isJSONMappingActive: boolean, + isFlycamRotated: boolean, ) => { if (!isSegmentationTracingVisible) { return "Volume annotation is disabled since no segmentation tracing layer is enabled. Enable one in the left settings sidebar or make a segmentation layer editable via the lock icon."; } + if (isFlycamRotated) { + return rotationActiveDisabledExplanation; + } + if (isZoomInvalidForTracing) { return "Volume annotation is disabled since the current zoom value is not in the required range. Please adjust the zoom level."; } @@ -125,7 +137,19 @@ function _getSkeletonToolInfo( } const getSkeletonToolInfo = memoizeOne(_getSkeletonToolInfo); -function _getBoundingBoxToolInfo(hasSkeleton: boolean, areGeometriesTransformed: boolean) { +function _getBoundingBoxToolInfo( + hasSkeleton: boolean, + areGeometriesTransformed: boolean, + isFlycamRotated: boolean, +) { + if (isFlycamRotated) { + return { + [AnnotationTool.BOUNDING_BOX.id]: { + isDisabled: true, + explanation: rotationActiveDisabledExplanation, + }, + }; + } if (areGeometriesTransformed) { return { [AnnotationTool.BOUNDING_BOX.id]: { @@ -155,6 +179,7 @@ function _getDisabledInfoWhenVolumeIsDisabled( isSegmentationTracingTransformed: boolean, isVolumeDisabled: boolean, isJSONMappingActive: boolean, + isFlycamRotated: boolean, ) { const genericDisabledExplanation = getExplanationForDisabledVolume( isSegmentationTracingVisible, @@ -164,6 +189,7 @@ function _getDisabledInfoWhenVolumeIsDisabled( isEditableMappingActive, isSegmentationTracingTransformed, isJSONMappingActive, + isFlycamRotated, ); const disabledInfo = { @@ -293,6 +319,7 @@ function getDisabledVolumeInfo(state: WebknossosState) { const { activeMappingByLayer } = state.temporaryConfiguration; const isZoomInvalidForTracing = isMagRestrictionViolated(state); const hasVolume = state.annotation.volumes.length > 0; + const isFlycamRotated = !_.isEqual(getRotationInRadian(state.flycam), [0, 0, 0]); const hasSkeleton = state.annotation.skeleton != null; const segmentationTracingLayer = getActiveSegmentationTracing(state); const labeledMag = getRenderableMagForSegmentationTracing(state, segmentationTracingLayer)?.mag; @@ -318,6 +345,7 @@ function getDisabledVolumeInfo(state: WebknossosState) { const isVolumeDisabled = !hasVolume || !isSegmentationTracingVisible || + isFlycamRotated || // isSegmentationTracingVisibleForMag is false if isZoomInvalidForTracing is true which is why // this condition doesn't need to be checked here !isSegmentationTracingVisibleForMag || @@ -340,6 +368,7 @@ function getDisabledVolumeInfo(state: WebknossosState) { isSegmentationTracingTransformed, isVolumeDisabled, isJSONMappingActive, + isFlycamRotated, ) : // Volume tools are not ALL disabled, but some of them might be. getVolumeDisabledWhenVolumeIsEnabled( @@ -360,13 +389,18 @@ const _getDisabledInfoForTools = ( ): Record => { const { annotation } = state; const hasSkeleton = annotation.skeleton != null; + const isFlycamRotated = !_.isEqual(getRotationInRadian(state.flycam), [0, 0, 0]); const geometriesTransformed = areGeometriesTransformed(state); const skeletonToolInfo = getSkeletonToolInfo( hasSkeleton, geometriesTransformed, isSkeletonLayerVisible(annotation), ); - const boundingBoxInfo = getBoundingBoxToolInfo(hasSkeleton, geometriesTransformed); + const boundingBoxInfo = getBoundingBoxToolInfo( + hasSkeleton, + geometriesTransformed, + isFlycamRotated, + ); const disabledVolumeInfo = getDisabledVolumeInfo(state); return { From 9870d2ecc5966590a69247e3beca764673de92aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 12 May 2025 19:26:06 +0200 Subject: [PATCH 018/128] fix new node rotation value calculation --- .../combinations/skeleton_handlers.ts | 18 +++++++++++++---- .../oxalis/controller/scene_controller.ts | 5 +++-- .../oxalis/model/accessors/flycam_accessor.ts | 20 +++++++++---------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts index 73f27d14761..945bebc368a 100644 --- a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts @@ -9,7 +9,8 @@ import { getEnabledColorLayers } from "oxalis/model/accessors/dataset_accessor"; import { getActiveMagIndicesForLayers, getPosition, - getRotationOrtho, + getRotationInRadian, + getRotationOrthoInRadian, isMagRestrictionViolated, } from "oxalis/model/accessors/flycam_accessor"; import { @@ -285,7 +286,17 @@ export function getOptionsForCreateSkeletonNode( const additionalCoordinates = state.flycam.additionalCoordinates; const skeletonTracing = enforceSkeletonTracing(state.annotation); const activeNode = getActiveNode(skeletonTracing); - const rotation = getRotationOrtho(activeViewport || state.viewModeData.plane.activeViewport); + const initialViewportRotation = getRotationOrthoInRadian( + activeViewport || state.viewModeData.plane.activeViewport, + ); + const flycamRotation = getRotationInRadian(state.flycam); + const totalRotationQuaternion = new THREE.Quaternion() + .setFromEuler(new THREE.Euler(...initialViewportRotation)) + .multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(...flycamRotation))); + const rotationEuler = new THREE.Euler().setFromQuaternion(totalRotationQuaternion); + const rotationInDegree = [rotationEuler.x, rotationEuler.y, rotationEuler.z].map( + (a) => (a * 180) / Math.PI, + ) as Vector3; // Center node if the corresponding setting is true. Only pressing CTRL can override this. const center = state.userConfiguration.centerNewNode && !ctrlIsPressed; @@ -299,10 +310,9 @@ export function getOptionsForCreateSkeletonNode( const activate = !ctrlIsPressed || activeNode == null; const skipCenteringAnimationInThirdDimension = true; - return { additionalCoordinates, - rotation, + rotation: rotationInDegree, center, branchpoint, activate, diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index df3e7bb6796..eb0365a6e09 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -431,8 +431,9 @@ class SceneController { // pos[ind[2]] += // planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; const unrotatedThirdDimOfPlane = [0, 0, 0]; - unrotatedThirdDimOfPlane[ind[2]] = - planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; + // TODO: Reenable + //unrotatedThirdDimOfPlane[ind[2]] = + // planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; const rotatedThirdDimOfPlane = new THREE.Vector3(...unrotatedThirdDimOfPlane).applyEuler( new THREE.Euler(...rotation), ); diff --git a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts index 9cb8a4254b9..a11a98b81a9 100644 --- a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts @@ -303,9 +303,9 @@ function _getRotationInRadianFixed(flycam: Flycam): Vector3 { object.applyMatrix4(matrix); const rotation: Vector3 = [object.rotation.x, object.rotation.y - Math.PI, object.rotation.z]; return [ - mod(rotation[0], Math.PI*2), - mod(rotation[1], Math.PI*2), - mod(rotation[2], Math.PI*2), + mod(rotation[0], Math.PI * 2), + mod(rotation[1], Math.PI * 2), + mod(rotation[2], Math.PI * 2), ]; } @@ -315,9 +315,9 @@ function _getRotationInRadian(flycam: Flycam): Vector3 { object.applyMatrix4(matrix); const rotation: Vector3 = [object.rotation.x, object.rotation.y, object.rotation.z - Math.PI]; return [ - mod(rotation[0], Math.PI*2), - mod(rotation[1], Math.PI*2), - mod(rotation[2], Math.PI*2), + mod(rotation[0], Math.PI * 2), + mod(rotation[1], Math.PI * 2), + mod(rotation[2], Math.PI * 2), ]; } @@ -328,7 +328,7 @@ function _getRotationInDegrees(flycam: Flycam): Vector3 { (180 / Math.PI) * rotationInRadian[0], (180 / Math.PI) * rotationInRadian[1], (180 / Math.PI) * rotationInRadian[2], - ] + ]; } function _getZoomedMatrix(flycam: Flycam): Matrix4x4 { @@ -518,13 +518,13 @@ export function getPlaneExtentInVoxel( const { width, height } = rects[planeID]; return [width * zoomStep, height * zoomStep]; } -export function getRotationOrtho(planeId: OrthoView): Vector3 { +export function getRotationOrthoInRadian(planeId: OrthoView): Vector3 { switch (planeId) { case OrthoViews.PLANE_YZ: - return [0, 270, 0]; + return [0, (3 / 2) * Math.PI, 0]; case OrthoViews.PLANE_XZ: - return [90, 0, 0]; + return [Math.PI / 2, 0, 0]; case OrthoViews.PLANE_XY: default: From dc5d3017a059b9c8589e37cde8ff1034c14fc9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 14 May 2025 14:03:00 +0200 Subject: [PATCH 019/128] fix rendering correct data by passing position offset to shader and calculate correct global position in shader --- .../viewer/controller/scene_controller.ts | 16 +++++++--------- .../materials/plane_material_factory.ts | 8 ++++---- .../javascripts/viewer/geometries/plane.ts | 11 ++++++----- .../javascripts/viewer/shaders/coords.glsl.ts | 18 ++++-------------- .../viewer/shaders/main_data_shaders.glsl.ts | 2 +- .../viewer/shaders/segmentation.glsl.ts | 2 +- 6 files changed, 23 insertions(+), 34 deletions(-) diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index c4cceccc646..226e8f0ee6c 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -426,19 +426,17 @@ class SceneController { const pos = _.clone(originalPosition); const ind = Dimensions.getIndices(planeId); - // Offset the plane so the user can see the skeletonTracing behind the plane - // TODOM: Somehow data is rendered slightly besides the borders due to this displacement. TODO: Investigate why - // pos[ind[2]] += - // planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; + // Offset the plane so the user can see the skeletonTracing behind the plane. + // The offset is passed to the shader as a uniform to be subtracted from the position to render the correct data. const unrotatedThirdDimOfPlane = [0, 0, 0]; - // TODO: Reenable - //unrotatedThirdDimOfPlane[ind[2]] = - // planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; + unrotatedThirdDimOfPlane[ind[2]] = + planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; const rotatedThirdDimOfPlane = new THREE.Vector3(...unrotatedThirdDimOfPlane).applyEuler( new THREE.Euler(...rotation), ); - const positionWithOffset = V3.add(pos, rotatedThirdDimOfPlane.toArray()); - this.planes[planeId].setPosition(positionWithOffset, originalPosition); + const rotatedPositionOffset = rotatedThirdDimOfPlane.toArray() as Vector3; + const positionWithOffset = V3.add(pos, rotatedPositionOffset); + this.planes[planeId].setPosition(positionWithOffset, rotatedPositionOffset); this.planes[planeId].setRotation(new THREE.Euler(rotation[0], rotation[1], rotation[2])); this.quickSelectGeometry.adaptVisibilityForRendering(originalPosition, ind[2]); diff --git a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts index 35be9aa80ae..db57759f48c 100644 --- a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts +++ b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts @@ -158,7 +158,7 @@ class PlaneMaterialFactory { is3DViewBeingRendered: { value: true, }, - globalPosition: { + positionOffset: { value: new THREE.Vector3(0, 0, 0), }, zoomValue: { @@ -447,9 +447,9 @@ class PlaneMaterialFactory { }; shaderEditor.addMaterial(this.shaderId, this.material); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'setGlobalPosition' does not exist on typ... Remove this comment to see the full error message - this.material.setGlobalPosition = (x, y, z) => { - this.uniforms.globalPosition.value.set(x, y, z); + // @ts-expect-error ts-migrate(2339) FIXME: Property 'setPositionOffset' does not exist on typ... Remove this comment to see the full error message + this.material.setPositionOffset = (x, y, z) => { + this.uniforms.positionOffset.value.set(x, y, z); }; // @ts-expect-error ts-migrate(2339) FIXME: Property 'setUseBilinearFiltering' does not exist ... Remove this comment to see the full error message diff --git a/frontend/javascripts/viewer/geometries/plane.ts b/frontend/javascripts/viewer/geometries/plane.ts index 8af42bc0917..d1ba91d1bec 100644 --- a/frontend/javascripts/viewer/geometries/plane.ts +++ b/frontend/javascripts/viewer/geometries/plane.ts @@ -28,6 +28,8 @@ import Store from "viewer/store"; // subdivision would probably be the next step. export const PLANE_SUBDIVISION = 100; +const DEFAULT_POSITION_OFFSET = [0, 0, 0]; + class Plane { // This class is supposed to collect all the Geometries that belong to one single plane such as // the plane itself, its texture, borders and crosshairs. @@ -172,18 +174,17 @@ class Plane { // In case the plane's position was offset to make geometries // on the plane visible (by moving the plane to the back), one can - // additionally pass the originalPosition (which is necessary for the + // additionally pass the offset of the position (which is necessary for the // shader) - setPosition = (pos: Vector3, originalPosition?: Vector3): void => { + setPosition = (pos: Vector3, positionOffset?: Vector3): void => { // TODOM: Write proper reasoning comment. const scaledPosition = V3.multiply(pos, this.datasetScaleFactor); this.TDViewBorders.position.set(...scaledPosition); this.crosshair[0].position.set(...scaledPosition); this.crosshair[1].position.set(...scaledPosition); this.plane.position.set(...scaledPosition); - - const scaledOriginalPosition = V3.multiply(originalPosition || pos, this.datasetScaleFactor); - this.plane.material.setGlobalPosition(...scaledOriginalPosition); + // Pass current plane offset to shader to calculate the position of the actual data that should be displayed (not the offsetted one). + this.plane.material.setPositionOffset(...(positionOffset || DEFAULT_POSITION_OFFSET)); }; setVisible = (isVisible: boolean, isDataVisible?: boolean): void => { diff --git a/frontend/javascripts/viewer/shaders/coords.glsl.ts b/frontend/javascripts/viewer/shaders/coords.glsl.ts index e78a8f6f71b..e10f713a79c 100644 --- a/frontend/javascripts/viewer/shaders/coords.glsl.ts +++ b/frontend/javascripts/viewer/shaders/coords.glsl.ts @@ -30,6 +30,7 @@ export const getWorldCoordUVW: ShaderModule = { code: ` vec3 getWorldCoordUVW() { vec3 worldCoordUVW = transDim(worldCoord.xyz); + vec3 positionOffsetUVW = transDim(positionOffset); if (isFlightMode()) { vec4 modelCoords = inverseMatrix(savedModelMatrix) * worldCoord; @@ -46,21 +47,10 @@ export const getWorldCoordUVW: ShaderModule = { vec3 voxelSizeFactorUVW = transDim(voxelSizeFactor); - worldCoordUVW = vec3( - // For u and w we need to divide by voxelSizeFactor because the threejs scene is scaled - worldCoordUVW.x / voxelSizeFactorUVW.x, - worldCoordUVW.y / voxelSizeFactorUVW.y, + // We need to divide by voxelSizeFactor because the threejs scene is scaled + // and then subtract the potential offset of the plane + worldCoordUVW = (worldCoordUVW / voxelSizeFactorUVW) - positionOffsetUVW; - // In orthogonal mode, the planes are offset in 3D space to allow skeletons to be rendered before - // each plane. Since w (e.g., z for xy plane) is - // the same for all texels computed in this shader, we simply use globalPosition[w] instead - // TODOM - //<% if (isOrthogonal) { %> - getW(globalPosition) - //<% } else { %> - worldCoordUVW.z / voxelSizeFactorUVW.z - //<% } %> - ); return worldCoordUVW; } diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index 0572ad6e942..e3c6cf14130 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -131,7 +131,7 @@ uniform float alpha; uniform bool renderBucketIndices; uniform vec3 bboxMin; uniform vec3 bboxMax; -uniform vec3 globalPosition; +uniform vec3 positionOffset; uniform vec3 activeSegmentPosition; uniform float zoomValue; uniform bool useBilinearFiltering; diff --git a/frontend/javascripts/viewer/shaders/segmentation.glsl.ts b/frontend/javascripts/viewer/shaders/segmentation.glsl.ts index 79ee458ce50..14062dd3c7c 100644 --- a/frontend/javascripts/viewer/shaders/segmentation.glsl.ts +++ b/frontend/javascripts/viewer/shaders/segmentation.glsl.ts @@ -327,7 +327,7 @@ export const getCrossHairOverlay: ShaderModule = { return vec4(0.0); } - vec3 flooredGlobalPosUVW = transDim(floor(globalPosition)); + vec3 flooredGlobalPosUVW = transDim(floor(worldCoordUVW)); vec3 activeSegmentPosUVW = transDim(activeSegmentPosition); // Compute the anisotropy of the dataset so that the cross hair looks the same in From 813028fb449386ce8ebf195a78f1e3538e0e2f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 14 May 2025 14:03:09 +0200 Subject: [PATCH 020/128] remove unused method --- frontend/javascripts/viewer/geometries/arbitrary_plane.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/javascripts/viewer/geometries/arbitrary_plane.ts b/frontend/javascripts/viewer/geometries/arbitrary_plane.ts index 6931c2b5163..305e6b39b37 100644 --- a/frontend/javascripts/viewer/geometries/arbitrary_plane.ts +++ b/frontend/javascripts/viewer/geometries/arbitrary_plane.ts @@ -44,11 +44,6 @@ class ArbitraryPlane { this.materialFactory.stopListening(); } - setPosition = (x: number, y: number, z: number) => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'setGlobalPosition' does not exist on typ... Remove this comment to see the full error message - this.meshes.mainPlane.material.setGlobalPosition(x, y, z); - }; - addToScene(scene: THREE.Scene) { _.values(this.meshes).forEach((mesh) => { if (mesh) { From c51c6b492b7b552e03217468c4b2ed8bac5f9804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 14 May 2025 15:41:49 +0200 Subject: [PATCH 021/128] make abstract tree tab a functional component --- .../right-border-tabs/abstract_tree_tab.tsx | 148 ++++++++---------- 1 file changed, 66 insertions(+), 82 deletions(-) diff --git a/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx b/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx index aa42350d93a..88ee099c09d 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx @@ -1,114 +1,98 @@ import { Button } from "antd"; +import { useWkSelector } from "libs/react_hooks"; import window from "libs/window"; import _ from "lodash"; -import React, { Component } from "react"; -import { connect } from "react-redux"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useDispatch } from "react-redux"; import type { Dispatch } from "redux"; import { setActiveNodeAction } from "viewer/model/actions/skeletontracing_actions"; -import type { SkeletonTracing, WebknossosState } from "viewer/store"; +import type { SkeletonTracing } from "viewer/store"; import type { NodeListItem } from "viewer/view/right-border-tabs/abstract_tree_renderer"; import AbstractTreeRenderer from "viewer/view/right-border-tabs/abstract_tree_renderer"; + type StateProps = { dispatch: Dispatch; skeletonTracing: SkeletonTracing | null | undefined; }; + type Props = StateProps; -type State = { - visible: boolean; -}; -class AbstractTreeTab extends Component { - canvas: HTMLCanvasElement | null | undefined; - nodeList: Array = []; - state: State = { - visible: false, - }; +const AbstractTreeTab: React.FC = () => { + const skeletonTracing = useWkSelector((state) => state.annotation.skeleton); + const canvasRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + const nodeListRef = useRef>([]); + const dispatch = useDispatch(); - componentDidMount() { - window.addEventListener("resize", this.drawTree, false); - this.drawTree(); - } + const drawTree = _.throttle( + useCallback(() => { + if (!skeletonTracing || !isVisible) { + return; + } - componentDidUpdate() { - this.drawTree(); - } + const { activeTreeId, activeNodeId, trees } = skeletonTracing; + const canvas = canvasRef.current; - componentWillUnmount() { - window.removeEventListener("resize", this.drawTree, false); - } + if (canvas) { + nodeListRef.current = AbstractTreeRenderer.drawTree( + canvas, + activeTreeId != null ? trees[activeTreeId] : null, + activeNodeId, + [canvas.offsetWidth, canvas.offsetHeight], + ); + } + }, [skeletonTracing, isVisible]), + 1000, + ); - drawTree = _.throttle(() => { - if (!this.props.skeletonTracing || !this.state.visible) { - return; - } + useEffect(() => { + window.addEventListener("resize", drawTree, false); + drawTree(); - const { activeTreeId, activeNodeId, trees } = this.props.skeletonTracing; - const { canvas } = this; + return () => { + window.removeEventListener("resize", drawTree, false); + }; + }, [drawTree]); - if (canvas != null) { - this.nodeList = AbstractTreeRenderer.drawTree( - canvas, - activeTreeId != null ? trees[activeTreeId] : null, - activeNodeId, - [canvas.offsetWidth, canvas.offsetHeight], - ); - } - }, 1000); + useEffect(() => { + drawTree(); + }, [drawTree]); - handleClick = (event: React.MouseEvent) => { + const handleClick = (event: React.MouseEvent) => { const id = AbstractTreeRenderer.getIdFromPos( event.nativeEvent.offsetX, event.nativeEvent.offsetY, - this.nodeList, + nodeListRef.current, ); if (id != null) { - this.props.dispatch(setActiveNodeAction(id)); + dispatch(setActiveNodeAction(id)); } }; - onClickShow = () => { - this.setState({ - visible: true, - }); - }; + const onClickShow = () => setIsVisible(true); - render() { - return ( -
- {this.state.visible ? ( - { - this.canvas = canvas; + return ( +
+ {isVisible ? ( + + ) : ( + + + - ) : ( - - - - This may be slow for very large tracings. - - - )} -
- ); - } -} - -function mapStateToProps(state: WebknossosState): Partial { - return { - skeletonTracing: state.annotation.skeleton, - }; -} + > + This may be slow for very large tracings. + + + )} +
+ ); +}; -const connector = connect(mapStateToProps); -export default connector(AbstractTreeTab); +export default AbstractTreeTab; From bafa502cd54d0d7aba087c7bfdd056ba932850bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 14 May 2025 15:56:04 +0200 Subject: [PATCH 022/128] suppress rotation of setting active node when in ortho view mode --- frontend/javascripts/viewer/api/api_latest.ts | 16 +++++++-- .../combinations/skeleton_handlers.ts | 33 +++++++++++++++---- .../viewmodes/arbitrary_controller.tsx | 4 ++- .../model/actions/skeletontracing_actions.tsx | 2 ++ .../model/helpers/action_logger_middleware.ts | 2 +- .../model/sagas/skeletontracing_saga.ts | 11 +++++-- .../javascripts/viewer/view/plane_view.ts | 1 + .../right-border-tabs/abstract_tree_tab.tsx | 25 ++++++++------ .../right-border-tabs/comment_tab/comment.tsx | 10 ++++-- .../comment_tab/comment_tab_view.tsx | 4 ++- .../javascripts/viewer/view/statusbar.tsx | 9 +++-- 11 files changed, 87 insertions(+), 30 deletions(-) diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index f93941335dd..8cc45155d52 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -278,10 +278,20 @@ class TracingApi { /** * Sets the active node given a node id. */ - setActiveNode(id: number) { - assertSkeleton(Store.getState().annotation); + setActiveNode( + id: number, + suppressAnimation?: boolean, + suppressCentering?: boolean, + suppressRotation?: boolean, + ) { + const { annotation, temporaryConfiguration } = Store.getState(); + assertSkeleton(annotation); assertExists(id, "Node id is missing."); - Store.dispatch(setActiveNodeAction(id)); + if (suppressRotation === undefined) { + // Per default disable setting rotation when orthogonal view is active. + suppressRotation = temporaryConfiguration.viewMode === "orthogonal"; + } + Store.dispatch(setActiveNodeAction(id, suppressAnimation, suppressCentering, suppressRotation)); } /** diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index f1655697918..22cff3e19df 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -99,7 +99,8 @@ export function handleSelectNode( // otherwise we have hit the background and do nothing if (nodeId != null && nodeId > 0) { - Store.dispatch(setActiveNodeAction(nodeId)); + const suppressRotation = "isOrthoPlaneView" in view && view.isOrthoPlaneView; + Store.dispatch(setActiveNodeAction(nodeId, false, false, suppressRotation)); return true; } @@ -478,7 +479,9 @@ function getPrecedingNodeFromTree( } export function toSubsequentNode(): void { - const tracing = enforceSkeletonTracing(Store.getState().annotation); + const { annotation, temporaryConfiguration } = Store.getState(); + const suppressRotation = temporaryConfiguration.viewMode === "orthogonal"; + const tracing = enforceSkeletonTracing(annotation); const { navigationList, activeNodeId, activeTreeId } = tracing; if (activeNodeId == null) return; const isValidList = @@ -490,7 +493,14 @@ export function toSubsequentNode(): void { isValidList ) { // navigate to subsequent node in list - Store.dispatch(setActiveNodeAction(navigationList.list[navigationList.activeIndex + 1])); + Store.dispatch( + setActiveNodeAction( + navigationList.list[navigationList.activeIndex + 1], + false, + false, + suppressRotation, + ), + ); Store.dispatch(updateNavigationListAction(navigationList.list, navigationList.activeIndex + 1)); } else { // search for subsequent node in tree @@ -505,14 +515,16 @@ export function toSubsequentNode(): void { if (nextNodeId !== activeNodeId) { newList.push(nextNodeId); - Store.dispatch(setActiveNodeAction(nextNodeId)); + Store.dispatch(setActiveNodeAction(nextNodeId, false, false, suppressRotation)); } Store.dispatch(updateNavigationListAction(newList, newList.length - 1)); } } export function toPrecedingNode(): void { - const tracing = enforceSkeletonTracing(Store.getState().annotation); + const { annotation, temporaryConfiguration } = Store.getState(); + const suppressRotation = temporaryConfiguration.viewMode === "orthogonal"; + const tracing = enforceSkeletonTracing(annotation); const { navigationList, activeNodeId, activeTreeId } = tracing; if (activeNodeId == null) return; const isValidList = @@ -520,7 +532,14 @@ export function toPrecedingNode(): void { if (navigationList.activeIndex > 0 && isValidList) { // navigate to preceding node in list - Store.dispatch(setActiveNodeAction(navigationList.list[navigationList.activeIndex - 1])); + Store.dispatch( + setActiveNodeAction( + navigationList.list[navigationList.activeIndex - 1], + false, + false, + suppressRotation, + ), + ); Store.dispatch(updateNavigationListAction(navigationList.list, navigationList.activeIndex - 1)); } else { // search for preceding node in tree @@ -535,7 +554,7 @@ export function toPrecedingNode(): void { if (nextNodeId !== activeNodeId) { newList.unshift(nextNodeId); - Store.dispatch(setActiveNodeAction(nextNodeId)); + Store.dispatch(setActiveNodeAction(nextNodeId, false, false, suppressRotation)); } Store.dispatch(updateNavigationListAction(newList, 0)); diff --git a/frontend/javascripts/viewer/controller/viewmodes/arbitrary_controller.tsx b/frontend/javascripts/viewer/controller/viewmodes/arbitrary_controller.tsx index 2adf8c7811a..a3769e11925 100644 --- a/frontend/javascripts/viewer/controller/viewmodes/arbitrary_controller.tsx +++ b/frontend/javascripts/viewer/controller/viewmodes/arbitrary_controller.tsx @@ -297,7 +297,9 @@ class ArbitraryController extends React.PureComponent { return; } // implicit cast from boolean to int - Store.dispatch(setActiveNodeAction(activeNode.id + 2 * Number(nextOne) - 1)); + Store.dispatch( + setActiveNodeAction(activeNode.id + 2 * Number(nextOne) - 1, false, false, false), + ); } move(timeFactor: number): void { diff --git a/frontend/javascripts/viewer/model/actions/skeletontracing_actions.tsx b/frontend/javascripts/viewer/model/actions/skeletontracing_actions.tsx index 91e90ff558d..e26bca9a2e2 100644 --- a/frontend/javascripts/viewer/model/actions/skeletontracing_actions.tsx +++ b/frontend/javascripts/viewer/model/actions/skeletontracing_actions.tsx @@ -239,12 +239,14 @@ export const setActiveNodeAction = ( nodeId: number, suppressAnimation: boolean = false, suppressCentering: boolean = false, + suppressRotation: boolean = true, ) => ({ type: "SET_ACTIVE_NODE", nodeId, suppressAnimation, suppressCentering, + suppressRotation, }) as const; export const centerActiveNodeAction = (suppressAnimation: boolean = false) => diff --git a/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts b/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts index 188930c22dc..0b1068aba1d 100644 --- a/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts +++ b/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts @@ -20,7 +20,7 @@ const actionBlacklist = [ "SET_INPUT_CATCHER_RECT", "SET_MOUSE_POSITION", "SET_POSITION", - "SET_ROTATION", + //"SET_ROTATION", "SET_TD_CAMERA", "SET_VIEWPORT", "ZOOM_TD_VIEW", diff --git a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts index 09a9056d28b..d7396831fe3 100644 --- a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts @@ -106,6 +106,7 @@ function* centerActiveNode(action: Action): Saga { const activeNode = getActiveNode( yield* select((state: WebknossosState) => enforceSkeletonTracing(state.annotation)), ); + const suppressRotation = "suppressRotation" in action && action.suppressRotation; if (activeNode != null) { const activeNodePosition = yield* select((state: WebknossosState) => @@ -113,9 +114,15 @@ function* centerActiveNode(action: Action): Saga { ); if ("suppressAnimation" in action && action.suppressAnimation) { Store.dispatch(setPositionAction(activeNodePosition)); - Store.dispatch(setRotationAction(activeNode.rotation)); + if (!suppressRotation) { + Store.dispatch(setRotationAction(activeNode.rotation)); + } } else { - api.tracing.centerPositionAnimated(activeNodePosition, false, activeNode.rotation); + api.tracing.centerPositionAnimated( + activeNodePosition, + false, + suppressRotation ? undefined : activeNode.rotation, + ); } if (activeNode.additionalCoordinates) { Store.dispatch(setAdditionalCoordinatesAction(activeNode.additionalCoordinates)); diff --git a/frontend/javascripts/viewer/view/plane_view.ts b/frontend/javascripts/viewer/view/plane_view.ts index 1bbf0fd4208..9a18dfe2927 100644 --- a/frontend/javascripts/viewer/view/plane_view.ts +++ b/frontend/javascripts/viewer/view/plane_view.ts @@ -56,6 +56,7 @@ class PlaneView { running: boolean; needsRerender: boolean; unsubscribeFunctions: Array<() => void> = []; + isOrthoPlaneView = true; constructor() { this.running = false; diff --git a/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx b/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx index 88ee099c09d..73c68528ab5 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx @@ -19,6 +19,7 @@ type Props = StateProps; const AbstractTreeTab: React.FC = () => { const skeletonTracing = useWkSelector((state) => state.annotation.skeleton); + const viewMode = useWkSelector((state) => state.temporaryConfiguration.viewMode); const canvasRef = useRef(null); const [isVisible, setIsVisible] = useState(false); const nodeListRef = useRef>([]); @@ -58,17 +59,21 @@ const AbstractTreeTab: React.FC = () => { drawTree(); }, [drawTree]); - const handleClick = (event: React.MouseEvent) => { - const id = AbstractTreeRenderer.getIdFromPos( - event.nativeEvent.offsetX, - event.nativeEvent.offsetY, - nodeListRef.current, - ); + const handleClick = useCallback( + (event: React.MouseEvent) => { + const id = AbstractTreeRenderer.getIdFromPos( + event.nativeEvent.offsetX, + event.nativeEvent.offsetY, + nodeListRef.current, + ); + const suppressRotation = viewMode === "orthogonal"; - if (id != null) { - dispatch(setActiveNodeAction(id)); - } - }; + if (id != null) { + dispatch(setActiveNodeAction(id, false, false, suppressRotation)); + } + }, + [dispatch, viewMode], + ); const onClickShow = () => setIsVisible(true); diff --git a/frontend/javascripts/viewer/view/right-border-tabs/comment_tab/comment.tsx b/frontend/javascripts/viewer/view/right-border-tabs/comment_tab/comment.tsx index a550fb7f444..7cfadbd35d5 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/comment_tab/comment.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/comment_tab/comment.tsx @@ -2,7 +2,9 @@ import { Popover } from "antd"; import type * as React from "react"; import classNames from "classnames"; +import { useWkSelector } from "libs/react_hooks"; import { document } from "libs/window"; +import { useCallback } from "react"; import { NODE_ID_REF_REGEX, POSITION_REF_REGEX } from "viewer/constants"; import { setActiveNodeAction } from "viewer/model/actions/skeletontracing_actions"; import type { CommentType } from "viewer/store"; @@ -54,9 +56,11 @@ function ActiveCommentPopover({ } export function Comment({ comment, isActive }: CommentProps) { - const handleClick = () => { - Store.dispatch(setActiveNodeAction(comment.nodeId)); - }; + const viewMode = useWkSelector((state) => state.temporaryConfiguration.viewMode); + const handleClick = useCallback(() => { + const suppressRotation = viewMode === "orthogonal"; + Store.dispatch(setActiveNodeAction(comment.nodeId, false, false, suppressRotation)); + }, [viewMode, comment.nodeId]); const liClassName = classNames("markdown", "markdown-small", "nowrap"); diff --git a/frontend/javascripts/viewer/view/right-border-tabs/comment_tab/comment_tab_view.tsx b/frontend/javascripts/viewer/view/right-border-tabs/comment_tab/comment_tab_view.tsx index fb3b3c8a175..2e30a24b75b 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/comment_tab/comment_tab_view.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/comment_tab/comment_tab_view.tsx @@ -107,6 +107,7 @@ function CommentTabView(props: Props) { const allowUpdate = useWkSelector((state) => state.annotation.restrictions.allowUpdate); const keyboardDelay = useWkSelector((state) => state.userConfiguration.keyboardDelay); + const viewMode = useWkSelector((state) => state.temporaryConfiguration.viewMode); const isAnnotationLockedByUser = useWkSelector((state) => state.annotation.isLockedByOwner); const isOwner = useWkSelector((state) => isAnnotationOwner(state)); @@ -217,7 +218,8 @@ function CommentTabView(props: Props) { previousCommentRef.current = previousComment; function setActiveNode(nodeId: number) { - dispatch(setActiveNodeAction(nodeId)); + const suppressRotation = viewMode === "orthogonal"; + dispatch(setActiveNodeAction(nodeId, false, false, suppressRotation)); } function deleteComment() { diff --git a/frontend/javascripts/viewer/view/statusbar.tsx b/frontend/javascripts/viewer/view/statusbar.tsx index 1783bf92a7c..933eed6e84c 100644 --- a/frontend/javascripts/viewer/view/statusbar.tsx +++ b/frontend/javascripts/viewer/view/statusbar.tsx @@ -415,6 +415,8 @@ function maybeLabelWithSegmentationWarning(isUint64SegmentationVisible: boolean, function Infos() { const isSkeletonAnnotation = useWkSelector((state) => state.annotation.skeleton != null); const activeVolumeTracing = useWkSelector((state) => getActiveSegmentationTracing(state)); + const activeViewMode = useWkSelector((state) => state.temporaryConfiguration.viewMode); + const activeCellId = activeVolumeTracing?.activeCellId; const activeNodeId = useWkSelector((state) => state.annotation.skeleton ? state.annotation.skeleton.activeNodeId : null, @@ -429,8 +431,11 @@ function Infos() { [dispatch], ); const onChangeActiveNodeId = useCallback( - (id: number) => dispatch(setActiveNodeAction(id)), - [dispatch], + (id: number) => { + const suppressRotation = activeViewMode === "orthogonal"; + dispatch(setActiveNodeAction(id, false, false, suppressRotation)); + }, + [dispatch, activeViewMode], ); const onChangeActiveTreeId = useCallback( (id: number) => dispatch(setActiveTreeAction(id)), From c9a4b27e9b53b6270c5b1baee88d22e8f2d434aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 15 May 2025 16:05:51 +0200 Subject: [PATCH 023/128] fix flickering when adjusting clipping distance --- .../viewer/controller/scene_controller.ts | 15 +++++++------- .../javascripts/viewer/geometries/plane.ts | 20 +++++++++++-------- .../javascripts/viewer/shaders/coords.glsl.ts | 2 +- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index 226e8f0ee6c..4b3226d32c5 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -428,15 +428,16 @@ class SceneController { const ind = Dimensions.getIndices(planeId); // Offset the plane so the user can see the skeletonTracing behind the plane. // The offset is passed to the shader as a uniform to be subtracted from the position to render the correct data. - const unrotatedThirdDimOfPlane = [0, 0, 0]; - unrotatedThirdDimOfPlane[ind[2]] = - planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]]; - const rotatedThirdDimOfPlane = new THREE.Vector3(...unrotatedThirdDimOfPlane).applyEuler( - new THREE.Euler(...rotation), + const unrotatedPositionOffset = [0, 0, 0]; + unrotatedPositionOffset[ind[2]] = Math.round( + planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]], ); - const rotatedPositionOffset = rotatedThirdDimOfPlane.toArray() as Vector3; + const rotatedPositionOffsetVector = new THREE.Vector3( + ...unrotatedPositionOffset, + ).applyEuler(new THREE.Euler(...rotation)); + const rotatedPositionOffset = rotatedPositionOffsetVector.toArray(); const positionWithOffset = V3.add(pos, rotatedPositionOffset); - this.planes[planeId].setPosition(positionWithOffset, rotatedPositionOffset); + this.planes[planeId].setPosition(positionWithOffset, pos); this.planes[planeId].setRotation(new THREE.Euler(rotation[0], rotation[1], rotation[2])); this.quickSelectGeometry.adaptVisibilityForRendering(originalPosition, ind[2]); diff --git a/frontend/javascripts/viewer/geometries/plane.ts b/frontend/javascripts/viewer/geometries/plane.ts index d1ba91d1bec..a580c6eb0dd 100644 --- a/frontend/javascripts/viewer/geometries/plane.ts +++ b/frontend/javascripts/viewer/geometries/plane.ts @@ -28,7 +28,7 @@ import Store from "viewer/store"; // subdivision would probably be the next step. export const PLANE_SUBDIVISION = 100; -const DEFAULT_POSITION_OFFSET = [0, 0, 0]; +const DEFAULT_POSITION_OFFSET = [0, 0, 0] as Vector3; class Plane { // This class is supposed to collect all the Geometries that belong to one single plane such as @@ -176,15 +176,19 @@ class Plane { // on the plane visible (by moving the plane to the back), one can // additionally pass the offset of the position (which is necessary for the // shader) - setPosition = (pos: Vector3, positionOffset?: Vector3): void => { + setPosition = (pos: Vector3, originalPosition?: Vector3): void => { // TODOM: Write proper reasoning comment. const scaledPosition = V3.multiply(pos, this.datasetScaleFactor); - this.TDViewBorders.position.set(...scaledPosition); - this.crosshair[0].position.set(...scaledPosition); - this.crosshair[1].position.set(...scaledPosition); - this.plane.position.set(...scaledPosition); - // Pass current plane offset to shader to calculate the position of the actual data that should be displayed (not the offsetted one). - this.plane.material.setPositionOffset(...(positionOffset || DEFAULT_POSITION_OFFSET)); + const scaledPositionRounded = V3.round(scaledPosition); + this.TDViewBorders.position.set(...scaledPositionRounded); + this.crosshair[0].position.set(...scaledPositionRounded); + this.crosshair[1].position.set(...scaledPositionRounded); + this.plane.position.set(...scaledPositionRounded); + // Pass current plane offset to shader to calculate the position of the actual data that should be displayed (not the offset position). + const scaledOriginalPosition = V3.multiply(originalPosition || pos, this.datasetScaleFactor); + const scaledOriginalPositionRounded = V3.round(scaledOriginalPosition); + const scaledOffset = V3.sub(scaledPositionRounded, scaledOriginalPositionRounded); + this.plane.material.setPositionOffset(...scaledOffset); }; setVisible = (isVisible: boolean, isDataVisible?: boolean): void => { diff --git a/frontend/javascripts/viewer/shaders/coords.glsl.ts b/frontend/javascripts/viewer/shaders/coords.glsl.ts index e10f713a79c..dd09b94e1a8 100644 --- a/frontend/javascripts/viewer/shaders/coords.glsl.ts +++ b/frontend/javascripts/viewer/shaders/coords.glsl.ts @@ -49,7 +49,7 @@ export const getWorldCoordUVW: ShaderModule = { // We need to divide by voxelSizeFactor because the threejs scene is scaled // and then subtract the potential offset of the plane - worldCoordUVW = (worldCoordUVW / voxelSizeFactorUVW) - positionOffsetUVW; + worldCoordUVW = (worldCoordUVW - positionOffsetUVW) / voxelSizeFactorUVW; return worldCoordUVW; From 07ba5574067169dc7fe87bc8e6fbff454a1c57c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 15 May 2025 17:02:29 +0200 Subject: [PATCH 024/128] fix clipping distance offsetting of planes leading to rendering errors --- .../viewer/controller/scene_controller.ts | 18 +++++--------- .../javascripts/viewer/geometries/plane.ts | 24 +++++++++---------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index 4b3226d32c5..a0648ece582 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -81,7 +81,7 @@ const getVisibleSegmentationLayerNames = reuseInstanceOnEquality((storeState: We class SceneController { skeletons: Record = {}; isPlaneVisible: OrthoViewMap; - planeShift: Vector3; + clippingDistance: number; datasetBoundingBox!: Cube; userBoundingBoxGroup!: THREE.Group; layerBoundingBoxGroup!: THREE.Group; @@ -112,7 +112,7 @@ class SceneController { [OrthoViews.PLANE_XZ]: true, [OrthoViews.TDView]: true, }; - this.planeShift = [0, 0, 0]; + this.clippingDistance = 0; this.segmentMeshController = new SegmentMeshController(); this.storePropertyUnsubscribers = []; } @@ -423,21 +423,17 @@ class SceneController { this.planes[planeId].setOriginalCrosshairColor(); this.planes[planeId].setVisible(!hidePlanes); - const pos = _.clone(originalPosition); - const ind = Dimensions.getIndices(planeId); // Offset the plane so the user can see the skeletonTracing behind the plane. // The offset is passed to the shader as a uniform to be subtracted from the position to render the correct data. const unrotatedPositionOffset = [0, 0, 0]; - unrotatedPositionOffset[ind[2]] = Math.round( - planeId === OrthoViews.PLANE_XY ? this.planeShift[ind[2]] : -this.planeShift[ind[2]], - ); + unrotatedPositionOffset[ind[2]] = + planeId === OrthoViews.PLANE_XY ? this.clippingDistance : -this.clippingDistance; const rotatedPositionOffsetVector = new THREE.Vector3( ...unrotatedPositionOffset, ).applyEuler(new THREE.Euler(...rotation)); const rotatedPositionOffset = rotatedPositionOffsetVector.toArray(); - const positionWithOffset = V3.add(pos, rotatedPositionOffset); - this.planes[planeId].setPosition(positionWithOffset, pos); + this.planes[planeId].setPosition(originalPosition, rotatedPositionOffset); this.planes[planeId].setRotation(new THREE.Euler(rotation[0], rotation[1], rotation[2])); this.quickSelectGeometry.adaptVisibilityForRendering(originalPosition, ind[2]); @@ -499,9 +495,7 @@ class SceneController { } setClippingDistance(value: number): void { - // convert nm to voxel - const voxelPerNMVector = getVoxelPerUnit(Store.getState().dataset.dataSource.scale); - V3.scale(voxelPerNMVector, value, this.planeShift); + this.clippingDistance = value; app.vent.emit("rerender"); } diff --git a/frontend/javascripts/viewer/geometries/plane.ts b/frontend/javascripts/viewer/geometries/plane.ts index a580c6eb0dd..e2e59c49d45 100644 --- a/frontend/javascripts/viewer/geometries/plane.ts +++ b/frontend/javascripts/viewer/geometries/plane.ts @@ -176,19 +176,19 @@ class Plane { // on the plane visible (by moving the plane to the back), one can // additionally pass the offset of the position (which is necessary for the // shader) - setPosition = (pos: Vector3, originalPosition?: Vector3): void => { + setPosition = ( + originalPosition: Vector3, + positionOffset: Vector3 = DEFAULT_POSITION_OFFSET, + ): void => { // TODOM: Write proper reasoning comment. - const scaledPosition = V3.multiply(pos, this.datasetScaleFactor); - const scaledPositionRounded = V3.round(scaledPosition); - this.TDViewBorders.position.set(...scaledPositionRounded); - this.crosshair[0].position.set(...scaledPositionRounded); - this.crosshair[1].position.set(...scaledPositionRounded); - this.plane.position.set(...scaledPositionRounded); - // Pass current plane offset to shader to calculate the position of the actual data that should be displayed (not the offset position). - const scaledOriginalPosition = V3.multiply(originalPosition || pos, this.datasetScaleFactor); - const scaledOriginalPositionRounded = V3.round(scaledOriginalPosition); - const scaledOffset = V3.sub(scaledPositionRounded, scaledOriginalPositionRounded); - this.plane.material.setPositionOffset(...scaledOffset); + const scaledPosition = V3.multiply(originalPosition, this.datasetScaleFactor); + // The offset is in screen space already so no scaling is necessary. + const offsetPosition = V3.add(scaledPosition, positionOffset); + this.TDViewBorders.position.set(...offsetPosition); + this.crosshair[0].position.set(...offsetPosition); + this.crosshair[1].position.set(...offsetPosition); + this.plane.position.set(...offsetPosition); + this.plane.material.setPositionOffset(...positionOffset); }; setVisible = (isVisible: boolean, isDataVisible?: boolean): void => { From 0cd92a32f0d2a949f50126bafa4702d92efe4d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 15 May 2025 17:10:07 +0200 Subject: [PATCH 025/128] add missing prop to arbitrary_view to be easily distinguishable to plane_view --- frontend/javascripts/viewer/view/arbitrary_view.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/javascripts/viewer/view/arbitrary_view.ts b/frontend/javascripts/viewer/view/arbitrary_view.ts index 23435220c16..6f302e7dd52 100644 --- a/frontend/javascripts/viewer/view/arbitrary_view.ts +++ b/frontend/javascripts/viewer/view/arbitrary_view.ts @@ -42,6 +42,7 @@ class ArbitraryView { group: THREE.Object3D; cameraPosition: Array; unsubscribeFunctions: Array<() => void> = []; + isOrthoPlaneView = false; constructor() { this.animate = this.animateImpl.bind(this); From 0cbcb7fb4f5c3b2a795fc9f9a193ef20ffdec0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 16 May 2025 09:36:23 +0200 Subject: [PATCH 026/128] less store.getState calls --- frontend/javascripts/viewer/api/api_latest.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index 8cc45155d52..a04311d6ebe 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -1400,13 +1400,16 @@ class TracingApi { skipCenteringAnimationInThirdDimension: boolean = true, rotation?: Vector3, ): void { - const { activeViewport } = Store.getState().viewModeData.plane; + const { viewModeData, flycam } = Store.getState(); + const { activeViewport } = viewModeData.plane; + const curPosition = getPosition(flycam); + const curRotation = getRotationInDegrees(flycam); + const isNotRotated = _.isEqual(curRotation, [0, 0, 0]); + // TODOM: Fix this 3rd dimension calculation. Otherwise centering will lead to slowly moving along the 3rd dimension and thus not staying in the slice when rotation is active and not axis aligned. const dimensionToSkip = - skipCenteringAnimationInThirdDimension && activeViewport !== OrthoViews.TDView + skipCenteringAnimationInThirdDimension && activeViewport !== OrthoViews.TDView && isNotRotated ? dimensions.thirdDimensionForPlane(activeViewport) : null; - const curPosition = getPosition(Store.getState().flycam); - const curRotation = getRotationInDegrees(Store.getState().flycam); if (!Array.isArray(rotation)) rotation = curRotation; rotation = this.getShortestRotation(curRotation, rotation); From 4c465aefe3403ec68d0d75026a2ab854d87bd1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 19 May 2025 13:50:35 +0200 Subject: [PATCH 027/128] fix node positioning in plane while rotation is active --- .../combinations/bounding_box_handlers.ts | 34 ++++++++++++------- .../controller/combinations/move_handlers.ts | 2 +- .../combinations/segmentation_handlers.ts | 6 ++-- .../combinations/skeleton_handlers.ts | 22 +++++------- .../controller/combinations/tool_controls.ts | 17 +++++----- .../combinations/volume_handlers.ts | 20 ++++++----- .../controller/viewmodes/plane_controller.tsx | 6 ++-- .../materials/plane_material_factory.ts | 2 +- .../model/accessors/view_mode_accessor.ts | 25 ++++++++------ .../viewer/model/sagas/volumetracing_saga.tsx | 3 +- .../javascripts/viewer/view/statusbar.tsx | 8 +++-- 11 files changed, 80 insertions(+), 65 deletions(-) diff --git a/frontend/javascripts/viewer/controller/combinations/bounding_box_handlers.ts b/frontend/javascripts/viewer/controller/combinations/bounding_box_handlers.ts index b84d77284a6..4a618b33440 100644 --- a/frontend/javascripts/viewer/controller/combinations/bounding_box_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/bounding_box_handlers.ts @@ -163,8 +163,8 @@ export function getClosestHoveredBoundingBox( ): [SelectedEdge, SelectedEdge | null | undefined] | null { const state = Store.getState(); const globalPosition = calculateMaybeGlobalPos(state, pos, plane); - - if (globalPosition == null) return null; + if (globalPosition == null || globalPosition.rounded == null) return null; + const roundedPosition = globalPosition.rounded; const { userBoundingBoxes } = getSomeTracing(state.annotation); const indices = Dimension.getIndices(plane); @@ -178,7 +178,7 @@ export function getClosestHoveredBoundingBox( for (const bbox of userBoundingBoxes) { const { min, max } = bbox.boundingBox; const isCrossSectionOfViewportVisible = - globalPosition[thirdDim] >= min[thirdDim] && globalPosition[thirdDim] < max[thirdDim]; + roundedPosition[thirdDim] >= min[thirdDim] && roundedPosition[thirdDim] < max[thirdDim]; if (!isCrossSectionOfViewportVisible || !bbox.isVisible) { continue; @@ -188,7 +188,7 @@ export function getClosestHoveredBoundingBox( // of how the indices of the array map to the visible bbox edges. const distanceArray = computeDistanceArray( bbox.boundingBox, - globalPosition, + globalPosition.rounded, indices, planeRatio, ); @@ -235,16 +235,15 @@ export function createBoundingBoxAndGetEdges( ): [SelectedEdge, SelectedEdge | null | undefined] | null { const state = Store.getState(); const globalPosition = calculateMaybeGlobalPos(state, pos, plane); - - if (globalPosition == null) return null; + if (globalPosition == null || globalPosition.rounded == null) return null; Store.dispatch( addUserBoundingBoxAction({ boundingBox: { - min: globalPosition, + min: globalPosition.rounded, // The last argument ensures that a Vector3 is used and not a // Float32Array. - max: V3.add(globalPosition, [1, 1, 1], [0, 0, 0]), + max: V3.add(globalPosition.rounded, [1, 1, 1], [0, 0, 0]), }, }), ); @@ -318,7 +317,7 @@ export function handleResizingBoundingBox( const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId); const bboxToResize = getBoundingBoxOfPrimaryEdge(primaryEdge, state); - if (!bboxToResize) { + if (!bboxToResize || globalMousePosition == null || globalMousePosition.rounded == null) { return; } @@ -327,10 +326,13 @@ export function handleResizingBoundingBox( max: [...bboxToResize.boundingBox.max] as Vector3, }; - function updateBoundsAccordingToEdge(edge: SelectedEdge): boolean { + function updateBoundsAccordingToEdge( + edge: SelectedEdge, + globalPositionRounded: Vector3, + ): boolean { const { resizableDimension } = edge; // For a horizontal edge only consider delta.y, for vertical only delta.x - const newPositionValue = Math.round(globalMousePosition[resizableDimension]); + const newPositionValue = Math.round(globalPositionRounded[resizableDimension]); const minOrMax = edge.isMaxEdge ? "max" : "min"; const oppositeOfMinOrMax = edge.isMaxEdge ? "min" : "max"; // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. @@ -356,14 +358,20 @@ export function handleResizingBoundingBox( } } - let didMinAndMaxEdgeSwitch = updateBoundsAccordingToEdge(primaryEdge); + let didMinAndMaxEdgeSwitch = updateBoundsAccordingToEdge( + primaryEdge, + globalMousePosition.rounded, + ); if (didMinAndMaxEdgeSwitch) { primaryEdge.isMaxEdge = !primaryEdge.isMaxEdge; } if (secondaryEdge) { - didMinAndMaxEdgeSwitch = updateBoundsAccordingToEdge(secondaryEdge); + didMinAndMaxEdgeSwitch = updateBoundsAccordingToEdge( + secondaryEdge, + globalMousePosition.rounded, + ); if (didMinAndMaxEdgeSwitch) { secondaryEdge.isMaxEdge = !secondaryEdge.isMaxEdge; diff --git a/frontend/javascripts/viewer/controller/combinations/move_handlers.ts b/frontend/javascripts/viewer/controller/combinations/move_handlers.ts index b3531ce98ee..a7952b81877 100644 --- a/frontend/javascripts/viewer/controller/combinations/move_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/move_handlers.ts @@ -94,7 +94,7 @@ function getMousePosition() { return calculateGlobalPos(state, { x: mousePosition[0], y: mousePosition[1], - }); + }).rounded; } export function zoomPlanes(value: number, zoomToMouse: boolean): void { diff --git a/frontend/javascripts/viewer/controller/combinations/segmentation_handlers.ts b/frontend/javascripts/viewer/controller/combinations/segmentation_handlers.ts index f2faf6bb9be..93e3ff227f2 100644 --- a/frontend/javascripts/viewer/controller/combinations/segmentation_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/segmentation_handlers.ts @@ -20,7 +20,7 @@ import Store from "viewer/store"; export async function handleAgglomerateSkeletonAtClick(clickPosition: Point2) { const state = Store.getState(); const globalPosition = calculateGlobalPos(state, clickPosition); - loadAgglomerateSkeletonAtPosition(globalPosition); + loadAgglomerateSkeletonAtPosition(globalPosition.rounded); } export async function loadAgglomerateSkeletonAtPosition(position: Vector3): Promise { const segmentation = Model.getVisibleSegmentationLayer(); @@ -78,10 +78,10 @@ export async function loadSynapsesOfAgglomerateAtPosition(position: Vector3) { export function handleClickSegment(clickPosition: Point2) { const state = Store.getState(); const globalPosition = calculateGlobalPos(state, clickPosition); - const segmentId = getSegmentIdForPosition(globalPosition); + const segmentId = getSegmentIdForPosition(globalPosition.rounded); const { additionalCoordinates } = state.flycam; if (segmentId > 0) { - Store.dispatch(clickSegmentAction(segmentId, globalPosition, additionalCoordinates)); + Store.dispatch(clickSegmentAction(segmentId, globalPosition.rounded, additionalCoordinates)); } } diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index 22cff3e19df..8409a99d28e 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -28,6 +28,7 @@ import { calculateGlobalPos, calculateMaybeGlobalPos, getInputCatcherRect, + type GlobalPosition, } from "viewer/model/accessors/view_mode_accessor"; import { setDirectionAction } from "viewer/model/actions/flycam_actions"; import { @@ -146,7 +147,7 @@ export function handleOpenContextMenu( const state = Store.getState(); // Use calculateMaybeGlobalPos instead of calculateGlobalPos, since calculateGlobalPos // only works for the data viewports, but this function is also called for the 3d viewport. - const globalPosition = calculateMaybeGlobalPos(state, position); + const globalPositions = calculateMaybeGlobalPos(state, position); const hoveredEdgesInfo = getClosestHoveredBoundingBox(position, plane); const clickedBoundingBoxId = hoveredEdgesInfo != null ? hoveredEdgesInfo[0].boxId : null; @@ -162,7 +163,7 @@ export function handleOpenContextMenu( event.pageY, nodeId, clickedBoundingBoxId, - globalPosition, + globalPositions?.rounded, activeViewport, meshId, meshIntersectionPosition, @@ -248,7 +249,7 @@ export function finishNodeMovement(nodeId: number) { } export function handleCreateNodeFromGlobalPosition( - position: Vector3, + nodePosition: GlobalPosition, activeViewport: OrthoView, ctrlIsPressed: boolean, ): void { @@ -269,7 +270,7 @@ export function handleCreateNodeFromGlobalPosition( skipCenteringAnimationInThirdDimension, } = getOptionsForCreateSkeletonNode(activeViewport, ctrlIsPressed); createSkeletonNode( - position, + nodePosition, additionalCoordinates, rotation, center, @@ -322,7 +323,7 @@ export function getOptionsForCreateSkeletonNode( } export function createSkeletonNode( - position: Vector3, + position: GlobalPosition, additionalCoordinates: AdditionalCoordinate[] | null, rotation: Vector3, center: boolean, @@ -330,7 +331,7 @@ export function createSkeletonNode( activate: boolean, skipCenteringAnimationInThirdDimension: boolean, ): void { - updateTraceDirection(position); + updateTraceDirection(position.rounded); let state = Store.getState(); const enabledColorLayers = getEnabledColorLayers(state.dataset, state.datasetConfiguration); @@ -344,7 +345,7 @@ export function createSkeletonNode( Store.dispatch( createNodeAction( - untransformNodePosition(position, state), + untransformNodePosition(position.floating, state), additionalCoordinates, rotation, OrthoViewToNumber[Store.getState().viewModeData.plane.activeViewport], @@ -369,12 +370,7 @@ export function createSkeletonNode( const treeAndNode = getTreeAndNode(newSkeleton, newNodeId, newSkeleton.activeTreeId); if (!treeAndNode) return; - const [_activeTree, newNode] = treeAndNode; - - api.tracing.centerPositionAnimated( - getNodePosition(newNode, state), - skipCenteringAnimationInThirdDimension, - ); + api.tracing.centerPositionAnimated(position.floating, skipCenteringAnimationInThirdDimension); } if (branchpoint) { diff --git a/frontend/javascripts/viewer/controller/combinations/tool_controls.ts b/frontend/javascripts/viewer/controller/combinations/tool_controls.ts index c2bf783df5a..5e91333bd1d 100644 --- a/frontend/javascripts/viewer/controller/combinations/tool_controls.ts +++ b/frontend/javascripts/viewer/controller/combinations/tool_controls.ts @@ -294,7 +294,7 @@ export class SkeletonToolController { if (plane) { const globalPosition = calculateGlobalPos(Store.getState(), pos); // SkeletonHandlers.handleCreateNodeFromEvent(pos, false); - api.tracing.createNode(globalPosition, { center: false }); + api.tracing.createNode(globalPosition.rounded, { center: false }); } } else { if ( @@ -761,7 +761,7 @@ export class QuickSelectToolController { const [h, s, l] = getSegmentColorAsHSLA(state, volumeTracing.activeCellId); const activeCellColor = new THREE.Color().setHSL(h, s, l); quickSelectGeometry.setColor(activeCellColor); - startPos = V3.floor(calculateGlobalPos(state, pos)); + startPos = V3.floor(calculateGlobalPos(state, pos).rounded); currentPos = startPos; isDragging = true; }, @@ -795,7 +795,7 @@ export class QuickSelectToolController { ) { return; } - const newCurrentPos = V3.floor(calculateGlobalPos(Store.getState(), pos)); + const newCurrentPos = V3.floor(calculateGlobalPos(Store.getState(), pos).rounded); if (event.shiftKey) { // If shift is held, the rectangle is resized on topLeft and bottomRight // so that the center is constant. @@ -814,7 +814,7 @@ export class QuickSelectToolController { }, leftClick: (pos: Point2, _plane: OrthoView, _event: MouseEvent, _isTouch: boolean) => { const state = Store.getState(); - const clickedPos = V3.floor(calculateGlobalPos(state, pos)); + const clickedPos = V3.floor(calculateGlobalPos(state, pos).rounded); isDragging = false; const quickSelectConfig = state.userConfiguration.quickSelect; @@ -895,7 +895,8 @@ export class LineMeasurementToolController { return; } const state = Store.getState(); - const newPos = V3.floor(calculateGlobalPos(state, pos, this.initialPlane)); + // TODOM: Maybe do not make this snap to voxel -> new issue + const newPos = V3.floor(calculateGlobalPos(state, pos, this.initialPlane).rounded); lineMeasurementGeometry.updateLatestPointPosition(newPos); Store.dispatch(setLastMeasuredAndViewportPositionAction(newPos, pos)); }; @@ -937,7 +938,7 @@ export class LineMeasurementToolController { } // Set a new measurement point. const state = Store.getState(); - const position = V3.floor(calculateGlobalPos(state, pos, plane)); + const position = V3.floor(calculateGlobalPos(state, pos, plane).rounded); if (!this.isMeasuring) { this.initialPlane = plane; lineMeasurementGeometry.setStartPoint(position, plane); @@ -1015,7 +1016,7 @@ export class AreaMeasurementToolController { return; } const state = Store.getState(); - const position = V3.floor(calculateGlobalPos(state, pos, this.initialPlane)); + const position = V3.floor(calculateGlobalPos(state, pos, this.initialPlane).rounded); areaMeasurementGeometry.addEdgePoint(position); Store.dispatch(setLastMeasuredAndViewportPositionAction(position, pos)); }, @@ -1080,7 +1081,7 @@ export class ProofreadToolController { } const state = Store.getState(); - const globalPosition = calculateGlobalPos(state, pos); + const globalPosition = calculateGlobalPos(state, pos).rounded; if (event.shiftKey) { Store.dispatch(proofreadMerge(globalPosition)); diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index 047974594b7..19087707de7 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -19,17 +19,18 @@ import { Model, Store, api } from "viewer/singletons"; export function handleDrawStart(pos: Point2, plane: OrthoView) { const state = Store.getState(); + const globalPosRounded = calculateGlobalPos(state, pos).rounded; Store.dispatch(setContourTracingModeAction(ContourModeEnum.DRAW)); - Store.dispatch(startEditingAction(calculateGlobalPos(state, pos), plane)); - Store.dispatch(addToLayerAction(calculateGlobalPos(state, pos))); + Store.dispatch(startEditingAction(globalPosRounded, plane)); + Store.dispatch(addToLayerAction(globalPosRounded)); } export function handleEraseStart(pos: Point2, plane: OrthoView) { Store.dispatch(setContourTracingModeAction(ContourModeEnum.DELETE)); - Store.dispatch(startEditingAction(calculateGlobalPos(Store.getState(), pos), plane)); + Store.dispatch(startEditingAction(calculateGlobalPos(Store.getState(), pos).rounded, plane)); } export function handleMoveForDrawOrErase(pos: Point2) { const state = Store.getState(); - Store.dispatch(addToLayerAction(calculateGlobalPos(state, pos))); + Store.dispatch(addToLayerAction(calculateGlobalPos(state, pos).rounded)); } export function handleEndForDrawOrErase() { Store.dispatch(finishEditingAction()); @@ -37,9 +38,12 @@ export function handleEndForDrawOrErase() { } export function handlePickCell(pos: Point2) { const storeState = Store.getState(); - const globalPos = calculateGlobalPos(storeState, pos); + const globalPosRounded = calculateGlobalPos(storeState, pos).rounded; - return handlePickCellFromGlobalPosition(globalPos, storeState.flycam.additionalCoordinates || []); + return handlePickCellFromGlobalPosition( + globalPosRounded, + storeState.flycam.additionalCoordinates || [], + ); } export const getSegmentIdForPosition = memoizeOne( (globalPos: Vector3) => { @@ -120,8 +124,8 @@ export function handlePickCellFromGlobalPosition( } } export function handleFloodFill(pos: Point2, plane: OrthoView) { - const globalPos = calculateGlobalPos(Store.getState(), pos); - handleFloodFillFromGlobalPosition(globalPos, plane); + const globalPosRounded = calculateGlobalPos(Store.getState(), pos).rounded; + handleFloodFillFromGlobalPosition(globalPosRounded, plane); } export function handleFloodFillFromGlobalPosition(globalPos: Vector3, plane: OrthoView) { Store.dispatch(floodFillAction(globalPos, plane)); diff --git a/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx b/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx index 2fb72a5b7fb..bf5c54d8422 100644 --- a/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx +++ b/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx @@ -489,14 +489,14 @@ class PlaneController extends React.PureComponent { if (mousePosition) { const [x, y] = mousePosition; - const globalMousePosition = calculateGlobalPos(Store.getState(), { + const globalMousePositionRounded = calculateGlobalPos(Store.getState(), { x, y, - }); + }).rounded; const { cube } = segmentationLayer; const mapping = event.altKey ? cube.getMapping() : null; const hoveredId = cube.getDataValue( - globalMousePosition, + globalMousePositionRounded, additionalCoordinates, mapping, getActiveMagIndexForLayer(Store.getState(), segmentationLayer.name), diff --git a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts index db57759f48c..50688d76b95 100644 --- a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts +++ b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts @@ -664,7 +664,7 @@ class PlaneMaterialFactory { const [x, y, z] = calculateGlobalPos(state, { x: globalMousePosition[0], y: globalMousePosition[1], - }); + }).rounded; this.uniforms.globalMousePosition.value.set(x, y, z); this.uniforms.isMouseInCanvas.value = true; }, diff --git a/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts b/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts index df1d75328fb..b33de246012 100644 --- a/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts @@ -92,12 +92,14 @@ export function getViewportScale(state: WebknossosState, viewport: Viewport): [n return [xScale, yScale]; } +export type GlobalPosition = { rounded: Vector3; floating: Vector3 }; + function _calculateMaybeGlobalPos( state: WebknossosState, clickPos: Point2, planeId?: OrthoView | null | undefined, -): Vector3 | null | undefined { - let position: Vector3; +): GlobalPosition | null | undefined { + let roundedPosition: Vector3, floatingPosition: Vector3; const planeIdFilled = planeId || state.viewModeData.plane.activeViewport; const curGlobalPos = getPosition(state.flycam); const flycamRotation = getRotationInRadian(state.flycam); @@ -123,10 +125,11 @@ function _calculateMaybeGlobalPos( .multiplyScalar(-1); const globalFloatingPosition = scaledRotatedPosition.applyMatrix4(flycamPositionMatrix); + floatingPosition = globalFloatingPosition.toArray() as Vector3; switch (planeIdFilled) { case OrthoViews.PLANE_XY: { - position = [ + roundedPosition = [ Math.round(globalFloatingPosition.x), Math.round(globalFloatingPosition.y), Math.floor(globalFloatingPosition.z), @@ -135,7 +138,7 @@ function _calculateMaybeGlobalPos( } case OrthoViews.PLANE_YZ: { - position = [ + roundedPosition = [ Math.floor(globalFloatingPosition.x), Math.round(globalFloatingPosition.y), Math.round(globalFloatingPosition.z), @@ -144,7 +147,7 @@ function _calculateMaybeGlobalPos( } case OrthoViews.PLANE_XZ: { - position = [ + roundedPosition = [ Math.round(globalFloatingPosition.x), Math.floor(globalFloatingPosition.y), Math.round(globalFloatingPosition.z), @@ -156,7 +159,7 @@ function _calculateMaybeGlobalPos( return null; } - return position; + return { rounded: roundedPosition, floating: floatingPosition }; } function _calculateMaybeGlobalDelta( @@ -197,15 +200,15 @@ function _calculateGlobalPos( state: WebknossosState, clickPos: Point2, planeId?: OrthoView | null | undefined, -): Vector3 { - const position = _calculateMaybeGlobalPos(state, clickPos, planeId); +): GlobalPosition { + const positions = _calculateMaybeGlobalPos(state, clickPos, planeId); - if (!position) { + if (!positions || !positions.rounded) { console.error("Trying to calculate the global position, but no data viewport is active."); - return [0, 0, 0]; + return { rounded: [0, 0, 0], floating: [0, 0, 0] }; } - return position; + return positions; } function _calculateGlobalDelta( diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index 8105886ebac..c2aae3552db 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -602,7 +602,8 @@ function* getGlobalMousePosition(): Saga { return calculateMaybeGlobalPos(state, { x, y, - }); + })?.rounded; + //TODOM: Might be better to use floating variant here for more accurate results, but maybe it is even more correct as the shader also enforces rounded positions. } return undefined; diff --git a/frontend/javascripts/viewer/view/statusbar.tsx b/frontend/javascripts/viewer/view/statusbar.tsx index 933eed6e84c..080f08511ca 100644 --- a/frontend/javascripts/viewer/view/statusbar.tsx +++ b/frontend/javascripts/viewer/view/statusbar.tsx @@ -574,7 +574,7 @@ function SegmentAndMousePosition() { const mousePosition = useWkSelector((state) => state.temporaryConfiguration.mousePosition); const additionalCoordinates = useWkSelector((state) => state.flycam.additionalCoordinates); const isPlaneMode = useWkSelector((state) => getIsPlaneMode(state)); - const globalMousePosition = useWkSelector((state) => { + const globalMousePositionRounded = useWkSelector((state) => { const { activeViewport } = state.viewModeData.plane; if (mousePosition && activeViewport !== OrthoViews.TDView) { @@ -582,7 +582,7 @@ function SegmentAndMousePosition() { return calculateGlobalPos(state, { x, y, - }); + }).rounded; } return undefined; @@ -594,7 +594,9 @@ function SegmentAndMousePosition() { {isPlaneMode ? ( Pos [ - {globalMousePosition ? getPosString(globalMousePosition, additionalCoordinates) : "-,-,-"} + {globalMousePositionRounded + ? getPosString(globalMousePositionRounded, additionalCoordinates) + : "-,-,-"} ] ) : null} From 263c06dd768eec90a31d0771f764c20e9e8c30f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 19 May 2025 14:01:01 +0200 Subject: [PATCH 028/128] WIP fix node creation --- frontend/javascripts/viewer/view/context_menu.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index c81658bc54d..9698bbe6c6e 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -1021,12 +1021,19 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] const isVolumeBasedToolActive = VolumeTools.includes(activeTool); const isBoundingBoxToolActive = activeTool === AnnotationTool.BOUNDING_BOX; + const globalPositionForNode = globalPosition + ? { rounded: globalPosition, floating: globalPosition } + : undefined; const skeletonActions: ItemType[] = - skeletonTracing != null && globalPosition != null && allowUpdate + skeletonTracing != null && + globalPosition != null && + globalPositionForNode != null && + allowUpdate ? [ { key: "create-node", - onClick: () => handleCreateNodeFromGlobalPosition(globalPosition, viewport, false), + onClick: () => + handleCreateNodeFromGlobalPosition(globalPositionForNode, viewport, false), label: "Create Node here", disabled: areGeometriesTransformed(state), }, @@ -1034,7 +1041,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] key: "create-node-with-tree", onClick: () => { Store.dispatch(createTreeAction()); - handleCreateNodeFromGlobalPosition(globalPosition, viewport, false); + handleCreateNodeFromGlobalPosition(globalPositionForNode, viewport, false); }, label: ( <> From 09d10adc72c88dec6b3bc1dbe5bbe17aabb496b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 19 May 2025 14:11:05 +0200 Subject: [PATCH 029/128] disallow bbox creating from context menu when rotation is active --- frontend/javascripts/viewer/view/context_menu.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index 22d17f998c2..8e92f1b73f4 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -141,6 +141,8 @@ import { withMappingActivationConfirmation, } from "viewer/view/right-border-tabs/segments_tab/segments_view_helper"; import { LoadMeshMenuItemLabel } from "./right-border-tabs/segments_tab/load_mesh_menu_item_label"; +import { getRotationInRadian } from "viewer/model/accessors/flycam_accessor"; +import _ from "lodash"; type ContextMenuContextValue = React.MutableRefObject | null; export const ContextMenuContext = createContext(null); @@ -171,6 +173,7 @@ type StateProps = { userBoundingBoxes: Array; mappingInfo: ActiveMappingInfo; allowUpdate: boolean; + isRotated: boolean; segments: SegmentMap | null | undefined; }; type Props = OwnProps & StateProps; @@ -780,6 +783,7 @@ function getBoundingBoxMenuOptions({ clickedBoundingBoxId, userBoundingBoxes, allowUpdate, + isRotated, }: NoNodeContextMenuProps): ItemType[] { if (globalPosition == null) return []; @@ -795,6 +799,8 @@ function getBoundingBoxMenuOptions({ {isBoundingBoxToolActive ? shortcutBuilder(["C"]) : null} ), + disabled: isRotated, + title: isRotated ? "Not available while view is rotated." : undefined, }; if (!allowUpdate && clickedBoundingBoxId != null) { @@ -1432,6 +1438,7 @@ function ContextMenuInner() { globalPosition: contextInfo.globalPosition, additionalCoordinates: state.flycam.additionalCoordinates || undefined, contextMenuPosition: contextInfo.contextMenuPosition, + isRotated: _.isEqual(getRotationInRadian(state.flycam), [0, 0, 0]), maybeViewport: contextInfo.viewport, maybeClickedMeshId: contextInfo.meshId, maybeMeshIntersectionPosition: contextInfo.meshIntersectionPosition, From fd17634fc92800def0a4ee6e06fa2da505b9f2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 19 May 2025 14:38:30 +0200 Subject: [PATCH 030/128] fix continuous node creation & some small typescript complaint --- frontend/javascripts/viewer/api/api_latest.ts | 15 ++++++++++----- .../controller/combinations/tool_controls.ts | 2 +- .../viewer/view/action-bar/tools/toolbar_view.tsx | 2 +- .../view/right-border-tabs/abstract_tree_tab.tsx | 12 ++---------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index a04311d6ebe..f02b05aaa6b 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -71,6 +71,7 @@ import { mapGroups, } from "viewer/model/accessors/skeletontracing_accessor"; import { AnnotationTool, type AnnotationToolId } from "viewer/model/accessors/tool_accessor"; +import type { GlobalPosition } from "viewer/model/accessors/view_mode_accessor"; import { getActiveCellId, getActiveSegmentationTracing, @@ -353,12 +354,14 @@ class TracingApi { } /** - * Creates a new node in the current tree. If the active tree - * is not empty, the node will be connected with an edge to - * the currently active node. + * Creates a new node in the current tree. If the active tree is not empty, + * the node will be connected with an edge to the currently active node. + * To keep optional the centering animation of the new node correct, + * the position can be passed as {rounded: [x,y,z], floating: [x,y,z]}, + * where floating is the not rounded more accurate position for a more precise annotation. */ createNode( - position: Vector3, + position: Vector3 | GlobalPosition, options?: { additionalCoordinates?: AdditionalCoordinate[]; rotation?: Vector3; @@ -368,10 +371,12 @@ class TracingApi { skipCenteringAnimationInThirdDimension?: boolean; }, ) { + const globalPosition = + "rounded" in position ? position : { rounded: position, floating: position }; assertSkeleton(Store.getState().annotation); const defaultOptions = getOptionsForCreateSkeletonNode(); createSkeletonNode( - position, + globalPosition, options?.additionalCoordinates ?? defaultOptions.additionalCoordinates, options?.rotation ?? defaultOptions.rotation, options?.center ?? defaultOptions.center, diff --git a/frontend/javascripts/viewer/controller/combinations/tool_controls.ts b/frontend/javascripts/viewer/controller/combinations/tool_controls.ts index 5e91333bd1d..48efad6301d 100644 --- a/frontend/javascripts/viewer/controller/combinations/tool_controls.ts +++ b/frontend/javascripts/viewer/controller/combinations/tool_controls.ts @@ -294,7 +294,7 @@ export class SkeletonToolController { if (plane) { const globalPosition = calculateGlobalPos(Store.getState(), pos); // SkeletonHandlers.handleCreateNodeFromEvent(pos, false); - api.tracing.createNode(globalPosition.rounded, { center: false }); + api.tracing.createNode(globalPosition, { center: false }); } } else { if ( diff --git a/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx b/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx index 3566e805a2b..c0c75dd6365 100644 --- a/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx +++ b/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx @@ -98,7 +98,7 @@ export default function ToolbarView() { return ( <> - + {Toolkits[toolkit].map((tool) => { const ToolButton = ToolIdToComponent[tool.id]; return ; diff --git a/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx b/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx index 73c68528ab5..2c66cdc03c5 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/abstract_tree_tab.tsx @@ -4,20 +4,12 @@ import window from "libs/window"; import _ from "lodash"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { useDispatch } from "react-redux"; -import type { Dispatch } from "redux"; +import type { EmptyObject } from "types/globals"; import { setActiveNodeAction } from "viewer/model/actions/skeletontracing_actions"; -import type { SkeletonTracing } from "viewer/store"; import type { NodeListItem } from "viewer/view/right-border-tabs/abstract_tree_renderer"; import AbstractTreeRenderer from "viewer/view/right-border-tabs/abstract_tree_renderer"; -type StateProps = { - dispatch: Dispatch; - skeletonTracing: SkeletonTracing | null | undefined; -}; - -type Props = StateProps; - -const AbstractTreeTab: React.FC = () => { +const AbstractTreeTab: React.FC = () => { const skeletonTracing = useWkSelector((state) => state.annotation.skeleton); const viewMode = useWkSelector((state) => state.temporaryConfiguration.viewMode); const canvasRef = useRef(null); From 76ce52b3afce35c7884ab4652900cb80d82937ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 19 May 2025 15:56:12 +0200 Subject: [PATCH 031/128] disable some context menu actions when rotation is active --- .../javascripts/viewer/view/context_menu.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index 8e92f1b73f4..6103294fe4e 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -409,12 +409,14 @@ function getMeshItems( visibleSegmentationLayer: APIDataLayer | null | undefined, voxelSizeFactor: Vector3, meshFileMappingName: string | null | undefined, + isRotated: boolean, ): MenuItemType[] { if ( clickedMeshId == null || meshIntersectionPosition == null || visibleSegmentationLayer == null || - volumeTracing == null + volumeTracing == null || + isRotated ) { return []; } @@ -572,10 +574,11 @@ function getNodeContextMenuOptions({ infoRows, allowUpdate, currentMeshFile, + isRotated, }: NodeContextMenuOptionsProps): ItemType[] { const state = Store.getState(); const isProofreadingActive = state.uiInformation.activeTool === AnnotationTool.PROOFREAD; - const isVolumeModificationAllowed = !hasEditableMapping(state); + const isVolumeModificationAllowed = !hasEditableMapping(state) && !isRotated; if (skeletonTracing == null) { throw new Error( @@ -614,6 +617,7 @@ function getNodeContextMenuOptions({ visibleSegmentationLayer, voxelSize.factor, currentMeshFile?.mappingName, + isRotated, ); const menuItems: ItemType[] = [ @@ -649,7 +653,7 @@ function getNodeContextMenuOptions({ ), }, - isProofreadingActive + isProofreadingActive && !isRotated ? { key: "min-cut-node", disabled: !areInSameTree || isTheSameNode, @@ -661,7 +665,7 @@ function getNodeContextMenuOptions({ } : null, - isProofreadingActive + isProofreadingActive && !isRotated ? { key: "cut-agglomerate-from-neighbors", disabled: !isProofreadingActive, @@ -951,6 +955,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] mappingInfo, infoRows, allowUpdate, + isRotated, } = props; const state = Store.getState(); @@ -1098,7 +1103,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] ), }, - isAgglomerateMappingEnabled.value + isAgglomerateMappingEnabled.value && !isRotated ? { key: "merge-agglomerate-skeleton", disabled: !isProofreadingActive, @@ -1116,7 +1121,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] ), } : null, - isAgglomerateMappingEnabled.value + isAgglomerateMappingEnabled.value && !isRotated ? { key: "min-cut-agglomerate-at-position", disabled: !isProofreadingActive, @@ -1136,7 +1141,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] ), } : null, - isAgglomerateMappingEnabled.value + isAgglomerateMappingEnabled.value && !isRotated ? { key: "cut-agglomerate-from-neighbors", disabled: !isProofreadingActive, @@ -1268,6 +1273,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] visibleSegmentationLayer, voxelSize.factor, currentMeshFile?.mappingName, + isRotated, ); if (isSkeletonToolActive) { From 11297c81229e6bba7e681e18f564445af9de9ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 19 May 2025 18:15:11 +0200 Subject: [PATCH 032/128] add rotated bucket prefetching --- .../prefetch_strategy_plane_rotated.ts | 271 ++++++++++++++++++ .../viewer/model/sagas/prefetch_saga.ts | 7 +- 2 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 frontend/javascripts/viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated.ts diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated.ts b/frontend/javascripts/viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated.ts new file mode 100644 index 00000000000..71ae1ddb488 --- /dev/null +++ b/frontend/javascripts/viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated.ts @@ -0,0 +1,271 @@ +import _ from "lodash"; +import type { AdditionalCoordinate } from "types/api_types"; +import type { OrthoView, OrthoViewMap, Vector3, Vector4 } from "viewer/constants"; +import constants, { OrthoViewValuesWithoutTDView } from "viewer/constants"; +import type { Area } from "viewer/model/accessors/flycam_accessor"; +import type DataCube from "viewer/model/bucket_data_handling/data_cube"; +import { getPriorityWeightForPrefetch } from "viewer/model/bucket_data_handling/loading_strategy_logic"; +import type { PullQueueItem } from "viewer/model/bucket_data_handling/pullqueue"; +import Dimensions from "viewer/model/dimensions"; +import { zoomedAddressToAnotherZoomStep } from "viewer/model/helpers/position_converter"; +import type { MagInfo } from "../helpers/mag_info"; +import * as THREE from "three"; +import { V3 } from "libs/mjs"; + +const { MAX_ZOOM_STEP_DIFF_PREFETCH } = constants; + +export enum ContentTypes { + SKELETON = "SKELETON", + VOLUME = "VOLUME", + READ_ONLY = "READ_ONLY", +} + +type Vector3PositionPropertyName = "x" | "y" | "z"; +const positionToVectorPropName: Record = { + 0: "x", + 1: "y", + 2: "z", +}; + +export class AbstractPrefetchStrategy { + velocityRangeStart: number = 0; + velocityRangeEnd: number = 0; + roundTripTimeRangeStart: number = 0; + roundTripTimeRangeEnd: number = 0; + contentTypes: Array = []; + name: string = "ABSTRACT"; + u: Vector3PositionPropertyName = "x"; + v: Vector3PositionPropertyName = "y"; + w: Vector3PositionPropertyName = "z"; + posVector: THREE.Vector3 = new THREE.Vector3(); + rotationMatrix: THREE.Matrix4 = new THREE.Matrix4(); + + forContentType(givenContentTypes: Record): boolean { + if (this.contentTypes.length === 0) { + return true; + } + + return this.contentTypes.some((contentType) => givenContentTypes[contentType]); + } + + inVelocityRange(value: number): boolean { + return this.velocityRangeStart <= value && value <= this.velocityRangeEnd; + } + + inRoundTripTimeRange(value: number): boolean { + return this.roundTripTimeRangeStart <= value && value <= this.roundTripTimeRangeEnd; + } + + getBucketPositions(center: Vector3, width: number, height: number): Array { + const buckets = []; + const uOffset = Math.ceil(width / 2); + const vOffset = Math.ceil(height / 2); + + for (let u = -uOffset; u <= uOffset; u++) { + for (let v = -vOffset; v <= vOffset; v++) { + // First rotated the bucket in local plane space. + this.posVector[this.u] = u; + this.posVector[this.v] = v; + this.posVector[this.w] = 0; + this.posVector.applyMatrix4(this.rotationMatrix); + // Then apply current flycam position offset to the bucket. + const bucket = [ + Math.round(this.posVector.x + center[0]), + Math.round(this.posVector.y + center[1]), + Math.round(this.posVector.z + center[2]), + ]; + + // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. + if (_.min(bucket) >= 0) { + buckets.push(bucket); + } + } + } + + // Typescript does not understand that slicing a Vector3 returns another Vector3 + // @ts-expect-error ts-migrate(2322) FIXME: Type 'number[][]' is not assignable to type 'Vecto... Remove this comment to see the full error message + return buckets; + } +} +export class PrefetchStrategy extends AbstractPrefetchStrategy { + velocityRangeStart = 0; + velocityRangeEnd = Number.POSITIVE_INFINITY; + roundTripTimeRangeStart = 0; + roundTripTimeRangeEnd = Number.POSITIVE_INFINITY; + preloadingSlides = 0; + preloadingPriorityOffset = 0; + + prefetch( + cube: DataCube, + position: Vector3, + rotation: Vector3, + direction: Vector3, + currentZoomStep: number, + activePlane: OrthoView, + areas: OrthoViewMap, + mags: Vector3[], + magInfo: MagInfo, + additionalCoordinates: AdditionalCoordinate[] | null, + ): Array { + const zoomStep = magInfo.getIndexOrClosestHigherIndex(currentZoomStep); + + if (zoomStep == null) { + // The layer cannot be rendered at this zoom step, as necessary magnifications + // are missing. Don't prefetch anything. + return []; + } + + const maxZoomStep = magInfo.getCoarsestMagIndex(); + const zoomStepDiff = currentZoomStep - zoomStep; + const queueItemsForCurrentZoomStep = this.prefetchImpl( + cube, + position, + rotation, + direction, + zoomStep, + zoomStepDiff, + activePlane, + areas, + mags, + false, + additionalCoordinates, + ); + let queueItemsForFallbackZoomStep: Array = []; + const fallbackZoomStep = Math.min(maxZoomStep, currentZoomStep + 1); + + if (fallbackZoomStep > zoomStep) { + queueItemsForFallbackZoomStep = this.prefetchImpl( + cube, + position, + rotation, + direction, + fallbackZoomStep, + zoomStepDiff - 1, + activePlane, + areas, + mags, + true, + additionalCoordinates, + ); + } + + return queueItemsForCurrentZoomStep.concat(queueItemsForFallbackZoomStep); + } + + prefetchImpl( + cube: DataCube, + position: Vector3, + rotation: Vector3, + direction: Vector3, + zoomStep: number, + zoomStepDiff: number, + activePlane: OrthoView, + areas: OrthoViewMap, + mags: Vector3[], + isFallback: boolean, + additionalCoordinates: AdditionalCoordinate[] | null, + ): Array { + const pullQueue: Array = []; + + if (zoomStepDiff > MAX_ZOOM_STEP_DIFF_PREFETCH) { + return pullQueue; + } + + this.rotationMatrix.makeRotationFromEuler( + new THREE.Euler(rotation[0], rotation[1], rotation[2]), + ); + + const centerBucket = cube.positionToZoomedAddress(position, additionalCoordinates, zoomStep); + const centerBucket3: Vector3 = [centerBucket[0], centerBucket[1], centerBucket[2]]; + const fallbackPriorityWeight = isFallback ? 50 : 0; + + for (const plane of OrthoViewValuesWithoutTDView) { + if (!areas[plane].isVisible) continue; + const [u, v, w] = Dimensions.getIndices(plane); + this.u = positionToVectorPropName[u]; + this.v = positionToVectorPropName[v]; + this.w = positionToVectorPropName[w]; + // areas holds bucket indices for zoomStep = 0, which we want to + // convert to the desired zoomStep + const widthHeightVector: Vector4 = [0, 0, 0, 0]; + widthHeightVector[u] = areas[plane].right - areas[plane].left; + widthHeightVector[v] = areas[plane].bottom - areas[plane].top; + const scaledWidthHeightVector = zoomedAddressToAnotherZoomStep( + widthHeightVector, + mags, + zoomStep, + ); + const width = scaledWidthHeightVector[u]; + const height = scaledWidthHeightVector[v]; + const bucketPositions = this.getBucketPositions(centerBucket3, width, height); + const prefetchWeight = getPriorityWeightForPrefetch(); + + for (const bucket of bucketPositions) { + const priority = + Math.abs(bucket[0] - centerBucket3[0]) + + Math.abs(bucket[1] - centerBucket3[1]) + + Math.abs(bucket[2] - centerBucket3[2]) + + prefetchWeight + + fallbackPriorityWeight; + + pullQueue.push({ + bucket: [bucket[0], bucket[1], bucket[2], zoomStep, additionalCoordinates ?? []], + priority, + }); + } + + // preload only for active plane + if (plane === activePlane) { + const getNewCenter = (offset: number): Vector3 => { + const centerOffset = Dimensions.transDim([0, 0, offset], plane); + const centerOffsetVector = new THREE.Vector3(...centerOffset); + const rotatedCenterOffset = centerOffsetVector.applyMatrix4(this.rotationMatrix); + return [ + Math.round(centerBucket3[0] + rotatedCenterOffset.x), + Math.round(centerBucket3[1] + rotatedCenterOffset.y), + Math.round(centerBucket3[2] + rotatedCenterOffset.z), + ]; + }; + const directionFactor = direction[Dimensions.thirdDimensionForPlane(plane)] >= 0 ? 1 : -1; + for (let slide = 0; slide < this.preloadingSlides; slide++) { + let centerWithOffset = getNewCenter(slide * directionFactor); + if (V3.equals(centerBucket3, centerWithOffset)) { + centerWithOffset = getNewCenter((slide + 1) * directionFactor); + } + const sliceOffsettedBucketPositions = this.getBucketPositions( + centerWithOffset, + width, + height, + ); + for (const bucket of sliceOffsettedBucketPositions) { + const priority = + Math.abs(bucket[0] - centerBucket3[0]) + + Math.abs(bucket[1] - centerBucket3[1]) + + Math.abs(bucket[2] - centerBucket3[2]) + + prefetchWeight + + fallbackPriorityWeight; + + const preloadingPriority = (priority << (slide + 1)) + this.preloadingPriorityOffset; + pullQueue.push({ + bucket: [bucket[0], bucket[1], bucket[2], zoomStep, additionalCoordinates ?? []], + priority: preloadingPriority, + }); + } + } + } + } + + return pullQueue; + } +} +export class PrefetchStrategySkeleton extends PrefetchStrategy { + contentTypes = [ContentTypes.SKELETON, ContentTypes.READ_ONLY]; + name = "SKELETON"; + preloadingSlides = 2; +} +export class PrefetchStrategyVolume extends PrefetchStrategy { + contentTypes = [ContentTypes.VOLUME]; + name = "VOLUME"; + preloadingSlides = 1; + preloadingPriorityOffset = 80; +} diff --git a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts index f9da4305726..b5679dfc241 100644 --- a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts @@ -7,6 +7,7 @@ import { getActiveMagIndexForLayer, getAreasFromState, getPosition, + getRotationInRadian, } from "viewer/model/accessors/flycam_accessor"; import { FlycamActions } from "viewer/model/actions/flycam_actions"; import { PrefetchStrategyArbitrary } from "viewer/model/bucket_data_handling/prefetch_strategy_arbitrary"; @@ -14,7 +15,7 @@ import { ContentTypes as PrefetchContentTypes, PrefetchStrategySkeleton, PrefetchStrategyVolume, -} from "viewer/model/bucket_data_handling/prefetch_strategy_plane"; +} from "viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated"; import { getGlobalDataConnectionInfo } from "viewer/model/data_connection_info"; import type DataLayer from "viewer/model/data_layer"; import type { Saga } from "viewer/model/sagas/effect-generators"; @@ -104,6 +105,7 @@ export function* prefetchForPlaneMode( previousProperties: Record, ): Saga { const position = yield* select((state) => getPosition(state.flycam)); + const rotation = yield* select((state) => getRotationInRadian(state.flycam)); const zoomStep = yield* select((state) => getActiveMagIndexForLayer(state, layer.name)); const magInfo = getMagInfo(layer.mags); const activePlane = yield* select((state) => state.viewModeData.plane.activeViewport); @@ -134,6 +136,7 @@ export function* prefetchForPlaneMode( const buckets = strategy.prefetch( layer.cube, position, + rotation, direction, zoomStep, activePlane, @@ -146,8 +149,8 @@ export function* prefetchForPlaneMode( if (WkDevFlags.bucketDebugging.visualizePrefetchedBuckets) { for (const item of buckets) { const bucket = layer.cube.getOrCreateBucket(item.bucket); - if (bucket.type !== "null") { + bucket.setVisualizationColor("green"); bucket.visualize(); } } From 58aa187d00d05481eb0a3b29557f795ccd639370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 20 May 2025 13:19:14 +0200 Subject: [PATCH 033/128] move back to old prefetcher and disable once rotation is active --- .../viewer/model/sagas/prefetch_saga.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts index b5679dfc241..326bb89f0a6 100644 --- a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts @@ -15,7 +15,7 @@ import { ContentTypes as PrefetchContentTypes, PrefetchStrategySkeleton, PrefetchStrategyVolume, -} from "viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated"; +} from "viewer/model/bucket_data_handling/prefetch_strategy_plane"; import { getGlobalDataConnectionInfo } from "viewer/model/data_connection_info"; import type DataLayer from "viewer/model/data_layer"; import type { Saga } from "viewer/model/sagas/effect-generators"; @@ -23,6 +23,7 @@ import { select } from "viewer/model/sagas/effect-generators"; import { Model } from "viewer/singletons"; import type { WebknossosState } from "viewer/store"; import { ensureWkReady } from "./ready_sagas"; +import _ from "lodash"; const PREFETCH_THROTTLE_TIME = 50; const DIRECTION_VECTOR_SMOOTHER = 0.125; @@ -45,14 +46,18 @@ export function* watchDataRelevantChanges(): Saga { function* shouldPrefetchForDataLayer(dataLayer: DataLayer): Saga { // There is no need to prefetch data for layers that are not visible - return yield* select((state) => - isLayerVisible( - state.dataset, - dataLayer.name, - state.datasetConfiguration, - state.temporaryConfiguration.viewMode, - ), - ); + return yield* select((state) => { + const isNotRotated = _.isEqual(getRotationInRadian(state.flycam), [0, 0, 0]); + return ( + isNotRotated && + isLayerVisible( + state.dataset, + dataLayer.name, + state.datasetConfiguration, + state.temporaryConfiguration.viewMode, + ) + ); + }); } export function* triggerDataPrefetching(previousProperties: Record): Saga { @@ -105,7 +110,6 @@ export function* prefetchForPlaneMode( previousProperties: Record, ): Saga { const position = yield* select((state) => getPosition(state.flycam)); - const rotation = yield* select((state) => getRotationInRadian(state.flycam)); const zoomStep = yield* select((state) => getActiveMagIndexForLayer(state, layer.name)); const magInfo = getMagInfo(layer.mags); const activePlane = yield* select((state) => state.viewModeData.plane.activeViewport); @@ -136,7 +140,6 @@ export function* prefetchForPlaneMode( const buckets = strategy.prefetch( layer.cube, position, - rotation, direction, zoomStep, activePlane, From c0edcd17219cff088aee0aa962c3bd0bdb9d6c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 20 May 2025 13:52:26 +0200 Subject: [PATCH 034/128] fix dataset navigation via space --- .../controller/combinations/move_handlers.ts | 15 +++++++-------- .../viewer/model/reducers/flycam_reducer.ts | 8 +++++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/javascripts/viewer/controller/combinations/move_handlers.ts b/frontend/javascripts/viewer/controller/combinations/move_handlers.ts index a7952b81877..4354063434d 100644 --- a/frontend/javascripts/viewer/controller/combinations/move_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/move_handlers.ts @@ -35,12 +35,12 @@ export const moveV = (deltaV: number): void => { movePlane([0, deltaV, 0]); }; export const moveW = (deltaW: number, oneSlide: boolean): void => { - if (is2dDataset(Store.getState().dataset)) { + const state = Store.getState(); + if (is2dDataset(state.dataset)) { return; } - const { activeViewport } = Store.getState().viewModeData.plane; - + const { activeViewport } = state.viewModeData.plane; if (activeViewport === OrthoViews.TDView) { return; } @@ -49,18 +49,17 @@ export const moveW = (deltaW: number, oneSlide: boolean): void => { // The following logic might not always make sense when having layers // that are transformed each. Todo: Rethink / adapt the logic once // problems occur. Tracked in #6926. - const { representativeMag } = getActiveMagInfo(Store.getState()); + const { representativeMag } = getActiveMagInfo(state); const wDim = Dimensions.getIndices(activeViewport)[2]; const wStep = (representativeMag || [1, 1, 1])[wDim]; Store.dispatch( - movePlaneFlycamOrthoAction( - [0, 0, Math.sign(deltaW) * Math.max(1, wStep)], + moveFlycamOrthoAction( + Dimensions.transDim([0, 0, Math.sign(deltaW) * Math.max(1, wStep)], activeViewport), activeViewport, - false, ), ); } else { - movePlane([0, 0, deltaW], false); + Store.dispatch(movePlaneFlycamOrthoAction([0, 0, deltaW], activeViewport, false)); } }; export function moveWhenAltIsPressed(delta: Point2, position: Point2, _id: any, event: MouseEvent) { diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 4d0b2013a14..7edba4c21b0 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -304,6 +304,12 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState const vector = _.clone(action.vector); const { planeId } = action; + const flycamRotation = getRotationInRadian(state.flycam); + + const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler( + new THREE.Euler(...flycamRotation), + ); + const movementVectorInWorld = new THREE.Vector3(...vector).applyMatrix4(rotationMatrix); // if planeID is given, use it to manipulate z if (planeId != null && state.userConfiguration.dynamicSpaceDirection) { @@ -312,7 +318,7 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState vector[dim] *= state.flycam.spaceDirectionOrtho[dim]; } - return moveReducer(state, vector); + return moveReducer(state, movementVectorInWorld.toArray()); } case "MOVE_PLANE_FLYCAM_ORTHO": { From a56fd85cfddf75c2d5e5eec58c8e4220429bba47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 20 May 2025 15:58:02 +0200 Subject: [PATCH 035/128] delete apparently useless file --- .../javascripts/types/schemas/url_state.types.ts | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 frontend/javascripts/types/schemas/url_state.types.ts diff --git a/frontend/javascripts/types/schemas/url_state.types.ts b/frontend/javascripts/types/schemas/url_state.types.ts deleted file mode 100644 index a7ad8df2787..00000000000 --- a/frontend/javascripts/types/schemas/url_state.types.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This file is only for documentation: -// Types which were used for creating the url_state.schema.js -// The `flow2schema` node module has been used for conversion. -// Unfortunately as of now, flow2schema doesn't support our code -// base out of the box so all types need to be temporarily copied into -// this file as imports do not work! -// -// Please note that some manual changes to the schema are required, -// i.e. the ::path::to::definition:: prefixes need to be removed -// and "additionalProperties: false," needs to be added to the objects -// to make the object types exact and forbid superfluous properties. -import type { UrlManagerState } from "viewer/controller/url_manager"; - -export type { UrlManagerState }; From 9994dd3dd0f10ca4cfc47ef7222663751050a13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 20 May 2025 15:58:51 +0200 Subject: [PATCH 036/128] fix node moving --- .../combinations/skeleton_handlers.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index 8409a99d28e..b4874778a6d 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -184,7 +184,8 @@ export function moveNode( useFloat: boolean = false, ) { // dx and dy are measured in pixel. - const skeletonTracing = getSkeletonTracing(Store.getState().annotation); + const state = Store.getState(); + const skeletonTracing = getSkeletonTracing(state.annotation); if (!skeletonTracing) return; const treeAndNode = getTreeAndNode(skeletonTracing, nodeId); @@ -192,14 +193,21 @@ export function moveNode( const [activeTree, activeNode] = treeAndNode; - const state = Store.getState(); const { activeViewport } = state.viewModeData.plane; const vector = Dimensions.transDim([dx, dy, 0], activeViewport); + const flycamRotation = getRotationInRadian(state.flycam); + const isRotated = !_.isEqual(flycamRotation, [0, 0, 0]); + + const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler( + new THREE.Euler(...flycamRotation), + ); + const vectorRotated = new THREE.Vector3(...vector).applyMatrix4(rotationMatrix); + const zoomFactor = state.flycam.zoomStep; const scaleFactor = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); const op = (val: number) => { - if (useFloat) { + if (useFloat || isRotated) { return val; } // Zero diffs should stay zero. @@ -215,9 +223,9 @@ export function moveNode( }; const delta = [ - op(vector[0] * zoomFactor * scaleFactor[0]), - op(vector[1] * zoomFactor * scaleFactor[1]), - op(vector[2] * zoomFactor * scaleFactor[2]), + op(vectorRotated.x * zoomFactor * scaleFactor[0]), + op(vectorRotated.y * zoomFactor * scaleFactor[1]), + op(vectorRotated.z * zoomFactor * scaleFactor[2]), ]; const [x, y, z] = getNodePosition(activeNode, state); From c12870c5f438568642701067877a7c5e58800afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 20 May 2025 15:59:11 +0200 Subject: [PATCH 037/128] make rotation copy button to rotation reset button --- frontend/javascripts/messages.tsx | 2 +- .../view/action-bar/dataset_position_view.tsx | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/javascripts/messages.tsx b/frontend/javascripts/messages.tsx index 16ce8f6c838..56784976bfd 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -161,7 +161,7 @@ instead. Only enable this option if you understand its effect. All layers will n "The current position is outside of the dataset's bounding box. No data will be shown here.", "tracing.out_of_task_bounds": "The current position is outside of the task's bounding box.", "tracing.copy_position": "Copy position to clipboard", - "tracing.copy_rotation": "Copy rotation to clipboard", + "tracing.reset_rotation": "Reset rotation", "tracing.copy_sharing_link": "Copy sharing link to clipboard", "tracing.tree_length_notification": (treeName: string, lengthInNm: string, lengthInVx: string) => `The tree ${treeName} has a total path length of ${lengthInNm} (${lengthInVx}).`, diff --git a/frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx b/frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx index e4e663f48ca..49e3b70cc72 100644 --- a/frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx +++ b/frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx @@ -1,4 +1,4 @@ -import { PushpinOutlined, ReloadOutlined } from "@ant-design/icons"; +import { PushpinOutlined, RollbackOutlined } from "@ant-design/icons"; import { Space } from "antd"; import FastTooltip from "components/fast_tooltip"; import { V3 } from "libs/mjs"; @@ -48,12 +48,6 @@ class DatasetPositionView extends PureComponent { Toast.success("Position copied to clipboard"); }; - copyRotationToClipboard = async () => { - const rotation = V3.round(getRotationInDegrees(this.props.flycam)).join(", "); - await navigator.clipboard.writeText(rotation); - Toast.success("Rotation copied to clipboard"); - }; - handleChangePosition = (position: Vector3) => { Store.dispatch(setPositionAction(position)); }; @@ -147,15 +141,15 @@ class DatasetPositionView extends PureComponent { marginLeft: 10, }} > - + this.handleChangeRotation([0, 0, 0])} style={{ padding: "0 10px", }} className="hide-on-small-screen" > - + Date: Tue, 20 May 2025 15:59:32 +0200 Subject: [PATCH 038/128] store rotation in url even in ortho mode --- .../viewer/controller/url_manager.ts | 10 ++++------ .../viewer/model_initialization.ts | 20 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/frontend/javascripts/viewer/controller/url_manager.ts b/frontend/javascripts/viewer/controller/url_manager.ts index 90ce78c7090..50bddf1d498 100644 --- a/frontend/javascripts/viewer/controller/url_manager.ts +++ b/frontend/javascripts/viewer/controller/url_manager.ts @@ -284,11 +284,9 @@ class UrlManager { const position: Vector3 = V3.floor(getPosition(state.flycam)); const { viewMode: mode } = state.temporaryConfiguration; const zoomStep = Utils.roundTo(state.flycam.zoomStep, 3); - const rotationOptional = constants.MODES_ARBITRARY.includes(mode) - ? { - rotation: Utils.map3((e) => Utils.roundTo(e, 2), getRotationInDegrees(state.flycam)), - } - : {}; + const rotation = { + rotation: Utils.map3((e) => Utils.roundTo(e, 2), getRotationInDegrees(state.flycam)), + }; const activeNode = state.annotation.skeleton?.activeNodeId; const activeNodeOptional = activeNode != null ? { activeNode } : {}; const stateByLayer: UrlStateByLayer = {}; @@ -375,7 +373,7 @@ class UrlManager { mode, zoomStep, additionalCoordinates: state.flycam.additionalCoordinates, - ...rotationOptional, + ...rotation, ...activeNodeOptional, ...stateByLayerOptional, }; diff --git a/frontend/javascripts/viewer/model_initialization.ts b/frontend/javascripts/viewer/model_initialization.ts index 9e3359a1d23..59be32d0718 100644 --- a/frontend/javascripts/viewer/model_initialization.ts +++ b/frontend/javascripts/viewer/model_initialization.ts @@ -661,8 +661,7 @@ function determineDefaultState( } = urlState; // If there is no editPosition (e.g. when viewing a dataset) and // no default position, compute the center of the dataset - const { dataset, datasetConfiguration, temporaryConfiguration } = Store.getState(); - const { viewMode } = temporaryConfiguration; + const { dataset, datasetConfiguration } = Store.getState(); const defaultPosition = datasetConfiguration.position; let position = getDatasetCenter(dataset); let additionalCoordinates = null; @@ -696,17 +695,14 @@ function determineDefaultState( zoomStep = urlStateZoomStep; } - let rotation = undefined; - if (viewMode !== "orthogonal") { - rotation = datasetConfiguration.rotation; - - if (someTracing != null) { - rotation = Utils.point3ToVector3(someTracing.editRotation); - } + let rotation = datasetConfiguration.rotation; + //TODOM: Now even in ortho mode the default rotation is set. Discuss whether thats what we actually want. + if (someTracing != null) { + rotation = Utils.point3ToVector3(someTracing.editRotation); + } - if (urlStateRotation != null) { - rotation = urlStateRotation; - } + if (urlStateRotation != null) { + rotation = urlStateRotation; } const stateByLayer = urlStateByLayer ?? {}; From d3ed2e1e1f9f4698958e0843783fb65cd0cd177a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 20 May 2025 16:51:52 +0200 Subject: [PATCH 039/128] add is rotated flycam accessor --- frontend/javascripts/viewer/api/api_latest.ts | 2 +- .../javascripts/viewer/model/accessors/flycam_accessor.ts | 4 ++++ frontend/javascripts/viewer/model/sagas/prefetch_saga.ts | 4 ++-- frontend/javascripts/viewer/view/context_menu.tsx | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index f02b05aaa6b..91736e1c706 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -1409,7 +1409,7 @@ class TracingApi { const { activeViewport } = viewModeData.plane; const curPosition = getPosition(flycam); const curRotation = getRotationInDegrees(flycam); - const isNotRotated = _.isEqual(curRotation, [0, 0, 0]); + const isNotRotated = V3.equals(curRotation, [0, 0, 0]); // TODOM: Fix this 3rd dimension calculation. Otherwise centering will lead to slowly moving along the 3rd dimension and thus not staying in the slice when rotation is active and not axis aligned. const dimensionToSkip = skipCenteringAnimationInThirdDimension && activeViewport !== OrthoViews.TDView && isNotRotated diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index ec14ba0dbdb..9abc2b9403a 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -331,6 +331,9 @@ function _getRotationInDegrees(flycam: Flycam): Vector3 { ]; } +function _isRotated(flycam: Flycam): boolean { + return !V3.equals(getRotationInRadian(flycam), [0, 0, 0]); +} function _getZoomedMatrix(flycam: Flycam): Matrix4x4 { return M4x4.scale1(flycam.zoomStep, flycam.currentMatrix); } @@ -342,6 +345,7 @@ export const getFlooredPosition = memoizeOne(_getFlooredPosition); export const getRotationInRadianFixed = memoizeOne(_getRotationInRadianFixed); export const getRotationInRadian = memoizeOne(_getRotationInRadian); export const getRotationInDegrees = memoizeOne(_getRotationInDegrees); +export const isRotated = memoizeOne(_isRotated); export const getZoomedMatrix = memoizeOne(_getZoomedMatrix); function _getActiveMagIndicesForLayers(state: WebknossosState): { [layerName: string]: number } { diff --git a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts index 326bb89f0a6..2105be7465e 100644 --- a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts @@ -7,7 +7,7 @@ import { getActiveMagIndexForLayer, getAreasFromState, getPosition, - getRotationInRadian, + isRotated, } from "viewer/model/accessors/flycam_accessor"; import { FlycamActions } from "viewer/model/actions/flycam_actions"; import { PrefetchStrategyArbitrary } from "viewer/model/bucket_data_handling/prefetch_strategy_arbitrary"; @@ -47,7 +47,7 @@ export function* watchDataRelevantChanges(): Saga { function* shouldPrefetchForDataLayer(dataLayer: DataLayer): Saga { // There is no need to prefetch data for layers that are not visible return yield* select((state) => { - const isNotRotated = _.isEqual(getRotationInRadian(state.flycam), [0, 0, 0]); + const isNotRotated = !isRotated(state.flycam); return ( isNotRotated && isLayerVisible( diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index 6103294fe4e..d7f58d55fc8 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -141,7 +141,7 @@ import { withMappingActivationConfirmation, } from "viewer/view/right-border-tabs/segments_tab/segments_view_helper"; import { LoadMeshMenuItemLabel } from "./right-border-tabs/segments_tab/load_mesh_menu_item_label"; -import { getRotationInRadian } from "viewer/model/accessors/flycam_accessor"; +import { isRotated } from "viewer/model/accessors/flycam_accessor"; import _ from "lodash"; type ContextMenuContextValue = React.MutableRefObject | null; @@ -1444,7 +1444,7 @@ function ContextMenuInner() { globalPosition: contextInfo.globalPosition, additionalCoordinates: state.flycam.additionalCoordinates || undefined, contextMenuPosition: contextInfo.contextMenuPosition, - isRotated: _.isEqual(getRotationInRadian(state.flycam), [0, 0, 0]), + isRotated: isRotated(state.flycam), maybeViewport: contextInfo.viewport, maybeClickedMeshId: contextInfo.meshId, maybeMeshIntersectionPosition: contextInfo.meshIntersectionPosition, From d451f8f7ef56f78f8dd76fcafaf23813441c4e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 20 May 2025 17:54:02 +0200 Subject: [PATCH 040/128] fix logging warning --- .../viewer/view/distance_measurement_tooltip.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx b/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx index d3d2207c667..d861ebdc6f6 100644 --- a/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx +++ b/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx @@ -47,7 +47,6 @@ export default function DistanceMeasurementTooltip() { ); const isMeasuring = useWkSelector((state) => state.uiInformation.measurementToolInfo.isMeasuring); const flycam = useWkSelector((state) => state.flycam); - const state = useWkSelector((state) => state); const activeTool = useWkSelector((state) => state.uiInformation.activeTool); const voxelSize = useWkSelector((state) => state.dataset.dataSource.scale); const tooltipRef = useRef(null); @@ -62,6 +61,12 @@ export default function DistanceMeasurementTooltip() { const orthoView = activeGeometry.viewport; // When the flycam is moved into the third dimension, the tooltip should be hidden. const thirdDim = dimensions.thirdDimensionForPlane(orthoView); + const { + left: viewportLeft, + top: viewportTop, + width: viewportWidth, + height: viewportHeight, + } = useWkSelector((state) => getInputCatcherRect(state, orthoView)); // biome-ignore lint/correctness/useExhaustiveDependencies(thirdDim): thirdDim is more or less a constant // biome-ignore lint/correctness/useExhaustiveDependencies(lastMeasuredGlobalPosition[thirdDim]): @@ -108,13 +113,6 @@ export default function DistanceMeasurementTooltip() { ); } - const { - left: viewportLeft, - top: viewportTop, - width: viewportWidth, - height: viewportHeight, - } = getInputCatcherRect(state, orthoView); - const tooltipWidth = tooltipRef.current?.offsetWidth ?? 0; const left = clamp( From dca68a09d779bc683688a3a24dbd52140065000f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 20 May 2025 17:56:33 +0200 Subject: [PATCH 041/128] disable area measurement while rotated --- .../combinations/skeleton_handlers.ts | 2 +- .../model/accessors/disabled_tool_accessor.ts | 19 +++++++++++++++++-- .../view/action-bar/tools/toolbar_view.tsx | 5 +++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index b4874778a6d..ec1d39f5427 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -196,7 +196,7 @@ export function moveNode( const { activeViewport } = state.viewModeData.plane; const vector = Dimensions.transDim([dx, dy, 0], activeViewport); const flycamRotation = getRotationInRadian(state.flycam); - const isRotated = !_.isEqual(flycamRotation, [0, 0, 0]); + const isRotated = V3.equals(flycamRotation, [0, 0, 0]); const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler( new THREE.Euler(...flycamRotation), diff --git a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts index 94f76cdcffb..ef24ba676c0 100644 --- a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts @@ -10,6 +10,7 @@ import { getVisibleSegmentationLayer } from "viewer/model/accessors/dataset_acce import { getRotationInRadian, isMagRestrictionViolated, + isRotated, } from "viewer/model/accessors/flycam_accessor"; import type { WebknossosState } from "viewer/store"; import { reuseInstanceOnEquality } from "./accessor_helpers"; @@ -95,9 +96,21 @@ const getExplanationForDisabledVolume = ( const ALWAYS_ENABLED_TOOL_INFOS = { [AnnotationTool.MOVE.id]: NOT_DISABLED_INFO, [AnnotationTool.LINE_MEASUREMENT.id]: NOT_DISABLED_INFO, - [AnnotationTool.AREA_MEASUREMENT.id]: NOT_DISABLED_INFO, }; +function _getAreaMeasurementToolInfo(isFlycamRotated: boolean) { + return { + [AnnotationTool.AREA_MEASUREMENT.id]: isFlycamRotated + ? { + isDisabled: true, + explanation: rotationActiveDisabledExplanation, + } + : NOT_DISABLED_INFO, + }; +} + +const getAreaMeasurementToolInfo = memoizeOne(_getAreaMeasurementToolInfo); + function _getSkeletonToolInfo( hasSkeleton: boolean, isSkeletonLayerTransformed: boolean, @@ -389,8 +402,9 @@ const _getDisabledInfoForTools = ( ): Record => { const { annotation } = state; const hasSkeleton = annotation.skeleton != null; - const isFlycamRotated = !_.isEqual(getRotationInRadian(state.flycam), [0, 0, 0]); + const isFlycamRotated = isRotated(state.flycam); const geometriesTransformed = areGeometriesTransformed(state); + const areaMeasurementToolInfo = getAreaMeasurementToolInfo(isFlycamRotated); const skeletonToolInfo = getSkeletonToolInfo( hasSkeleton, geometriesTransformed, @@ -405,6 +419,7 @@ const _getDisabledInfoForTools = ( const disabledVolumeInfo = getDisabledVolumeInfo(state); return { ...ALWAYS_ENABLED_TOOL_INFOS, + ...areaMeasurementToolInfo, ...skeletonToolInfo, ...disabledVolumeInfo, ...boundingBoxInfo, diff --git a/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx b/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx index c0c75dd6365..2853c7d479d 100644 --- a/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx +++ b/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx @@ -36,6 +36,7 @@ import { QuickSelectSettingsPopover, VolumeInterpolationButton, } from "./volume_specific_ui"; +import { getDisabledInfoForTools } from "viewer/model/accessors/disabled_tool_accessor"; const handleAddNewUserBoundingBox = () => { Store.dispatch(addUserBoundingBoxAction()); @@ -234,6 +235,8 @@ function ToolSpecificSettings({ function MeasurementToolSwitch({ activeTool }: { activeTool: AnnotationTool }) { const dispatch = useDispatch(); + const disabledInfosForTools = useWkSelector(getDisabledInfoForTools); + const { isDisabled, explanation } = disabledInfosForTools[AnnotationTool.AREA_MEASUREMENT.id]; const handleSetMeasurementTool = (evt: RadioChangeEvent) => { dispatch(setToolAction(evt.target.value)); @@ -254,11 +257,13 @@ function MeasurementToolSwitch({ activeTool }: { activeTool: AnnotationTool }) { Measurement Tool Icon Date: Tue, 20 May 2025 17:57:07 +0200 Subject: [PATCH 042/128] do not have duplicate area measurement points --- frontend/javascripts/viewer/geometries/helper_geometries.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/javascripts/viewer/geometries/helper_geometries.ts b/frontend/javascripts/viewer/geometries/helper_geometries.ts index 09115f93a01..a9df66d4b26 100644 --- a/frontend/javascripts/viewer/geometries/helper_geometries.ts +++ b/frontend/javascripts/viewer/geometries/helper_geometries.ts @@ -72,6 +72,12 @@ export class ContourGeometry { } addEdgePoint(pos: Vector3) { + const pointCount = this.vertexBuffer.getLength(); + const lastPoint = this.vertexBuffer.getBuffer().subarray((pointCount - 1) * 3, pointCount * 3); + if (V3.equals(pos, lastPoint)) { + // Skip adding the point if it is the same as the last one. + return; + } this.vertexBuffer.push(pos); const startPoint = this.vertexBuffer.getBuffer().subarray(0, 3); // Setting start and end point to form the connecting line. From be60117305025c697afdcd00eeaa8c7457936d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 21 May 2025 13:05:10 +0200 Subject: [PATCH 043/128] precompute inverted voxelSizeFactor to avoid floating point imprecisions --- .../viewer/geometries/materials/plane_material_factory.ts | 4 ++++ frontend/javascripts/viewer/shaders/coords.glsl.ts | 8 ++++---- .../javascripts/viewer/shaders/main_data_shaders.glsl.ts | 7 +++++-- .../javascripts/viewer/shaders/thin_plate_spline.glsl.ts | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts index 7f9205c4dce..d4b8fd1f6ea 100644 --- a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts +++ b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts @@ -1081,6 +1081,7 @@ class PlaneMaterialFactory { const textureLayerInfos = getTextureLayerInfos(); const { dataset } = Store.getState(); const voxelSizeFactor = dataset.dataSource.scale.factor; + const voxelSizeFactorInverted = V3.divide3([1, 1, 1], voxelSizeFactor); const code = getMainFragmentShader({ globalLayerCount, orderedColorLayerNames, @@ -1089,6 +1090,7 @@ class PlaneMaterialFactory { textureLayerInfos, magnificationsCount: this.getTotalMagCount(), voxelSizeFactor, + voxelSizeFactorInverted, isOrthogonal: this.isOrthogonal, tpsTransformPerLayer: this.scaledTpsInvPerLayer, }); @@ -1115,6 +1117,7 @@ class PlaneMaterialFactory { const textureLayerInfos = getTextureLayerInfos(); const { dataset } = Store.getState(); const voxelSizeFactor = dataset.dataSource.scale.factor; + const voxelSizeFactorInverted = V3.divide3([1, 1, 1], voxelSizeFactor); return getMainVertexShader({ globalLayerCount, @@ -1124,6 +1127,7 @@ class PlaneMaterialFactory { textureLayerInfos, magnificationsCount: this.getTotalMagCount(), voxelSizeFactor, + voxelSizeFactorInverted, isOrthogonal: this.isOrthogonal, tpsTransformPerLayer: this.scaledTpsInvPerLayer, }); diff --git a/frontend/javascripts/viewer/shaders/coords.glsl.ts b/frontend/javascripts/viewer/shaders/coords.glsl.ts index dd09b94e1a8..19ac35215b5 100644 --- a/frontend/javascripts/viewer/shaders/coords.glsl.ts +++ b/frontend/javascripts/viewer/shaders/coords.glsl.ts @@ -45,11 +45,11 @@ export const getWorldCoordUVW: ShaderModule = { worldCoordUVW = (savedModelMatrix * modelCoords).xyz; } - vec3 voxelSizeFactorUVW = transDim(voxelSizeFactor); + vec3 voxelSizeFactorInvertedUVW = transDim(voxelSizeFactorInverted); - // We need to divide by voxelSizeFactor because the threejs scene is scaled - // and then subtract the potential offset of the plane - worldCoordUVW = (worldCoordUVW - positionOffsetUVW) / voxelSizeFactorUVW; + // We subtract the potential offset of the plane and then + // need to multiply by voxelSizeFactorInvertedUVW because the threejs scene is scaled. + worldCoordUVW = (worldCoordUVW - positionOffsetUVW) * voxelSizeFactorInvertedUVW; return worldCoordUVW; diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index e79d7d3ce16..60b7629daf2 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -58,6 +58,7 @@ export type Params = { >; magnificationsCount: number; voxelSizeFactor: Vector3; + voxelSizeFactorInverted: Vector3; isOrthogonal: boolean; tpsTransformPerLayer: Record; }; @@ -150,6 +151,7 @@ uniform uint hoveredUnmappedSegmentIdHigh; // rendering of the brush circle (and issues in the arbitrary modes). That's why it // is directly inserted into the source via templating. const vec3 voxelSizeFactor = <%= formatVector3AsVec3(voxelSizeFactor) %>; +const vec3 voxelSizeFactorInverted = <%= formatVector3AsVec3(voxelSizeFactorInverted) %>; const vec4 fallbackGray = vec4(0.5, 0.5, 0.5, 1.0); const float bucketWidth = <%= bucketWidth %>; @@ -512,13 +514,14 @@ void main() { vec2 d = transDim(vec3(bucketWidth) * representativeMagForVertexAlignment).xy; vec3 voxelSizeFactorUVW = transDim(voxelSizeFactor); + vec3 voxelSizeFactorInvertedUVW = transDim(voxelSizeFactorInverted); vec3 transWorldCoord = transDim(worldCoord.xyz); if (index.x >= 1. && index.x <= PLANE_SUBDIVISION - 1.) { transWorldCoord.x = ( // Left border of left-most bucket (probably outside of visible plane) - floor(worldCoordTopLeft.x / voxelSizeFactorUVW.x / d.x) * d.x + floor(worldCoordTopLeft.x * voxelSizeFactorInvertedUVW.x / d.x) * d.x // Move by index.x buckets to the right. + index.x * d.x ) * voxelSizeFactorUVW.x; @@ -530,7 +533,7 @@ void main() { transWorldCoord.y = ( // Top border of top-most bucket (probably outside of visible plane) - floor(worldCoordTopLeft.y / voxelSizeFactorUVW.y / d.y) * d.y + floor(worldCoordTopLeft.y * voxelSizeFactorInvertedUVW.y / d.y) * d.y // Move by index.y buckets to the bottom. + index.y * d.y ) * voxelSizeFactorUVW.y; diff --git a/frontend/javascripts/viewer/shaders/thin_plate_spline.glsl.ts b/frontend/javascripts/viewer/shaders/thin_plate_spline.glsl.ts index 31db3a1aff1..f89b6580b9e 100644 --- a/frontend/javascripts/viewer/shaders/thin_plate_spline.glsl.ts +++ b/frontend/javascripts/viewer/shaders/thin_plate_spline.glsl.ts @@ -74,7 +74,7 @@ export function generateCalculateTpsOffsetFunction(name: string) { bending_part += dist * TPS_W_${name}[cpIdx]; } - vec3 offset = (linear_part + bending_part) / voxelSizeFactor; + vec3 offset = (linear_part + bending_part) * voxelSizeFactorInverted; return offset; } `; From e633eb2f506f6d162b9612c2f56dafcd086173cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 21 May 2025 13:16:31 +0200 Subject: [PATCH 044/128] fix flickering when changing clipping distance by flooring & remove unused imports --- frontend/javascripts/viewer/controller/scene_controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index a0648ece582..450757e34c7 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -1,5 +1,4 @@ import app from "app"; -import { V3 } from "libs/mjs"; import Toast from "libs/toast"; import * as Utils from "libs/utils"; import window from "libs/window"; @@ -59,7 +58,6 @@ import { sceneControllerReadyAction } from "viewer/model/actions/actions"; import Dimensions from "viewer/model/dimensions"; import { listenToStoreProperty } from "viewer/model/helpers/listener_helpers"; import type { Transform } from "viewer/model/helpers/transformation_helpers"; -import { getVoxelPerUnit } from "viewer/model/scaleinfo"; import { Model } from "viewer/singletons"; import type { SkeletonTracing, UserBoundingBox, WebknossosState } from "viewer/store"; import Store from "viewer/store"; @@ -428,7 +426,9 @@ class SceneController { // The offset is passed to the shader as a uniform to be subtracted from the position to render the correct data. const unrotatedPositionOffset = [0, 0, 0]; unrotatedPositionOffset[ind[2]] = - planeId === OrthoViews.PLANE_XY ? this.clippingDistance : -this.clippingDistance; + planeId === OrthoViews.PLANE_XY + ? Math.floor(this.clippingDistance) + : Math.floor(-this.clippingDistance); const rotatedPositionOffsetVector = new THREE.Vector3( ...unrotatedPositionOffset, ).applyEuler(new THREE.Euler(...rotation)); From f7b039b74b63df24a9b9233ad0745bafc0fc622a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 21 May 2025 13:21:23 +0200 Subject: [PATCH 045/128] format frontend --- .../viewer/controller/combinations/skeleton_handlers.ts | 2 +- .../bucket_data_handling/prefetch_strategy_plane_rotated.ts | 4 ++-- frontend/javascripts/viewer/model/sagas/prefetch_saga.ts | 1 - .../javascripts/viewer/view/action-bar/tools/toolbar_view.tsx | 2 +- frontend/javascripts/viewer/view/context_menu.tsx | 3 +-- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index ec1d39f5427..fdd217ebe55 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -25,10 +25,10 @@ import { untransformNodePosition, } from "viewer/model/accessors/skeletontracing_accessor"; import { + type GlobalPosition, calculateGlobalPos, calculateMaybeGlobalPos, getInputCatcherRect, - type GlobalPosition, } from "viewer/model/accessors/view_mode_accessor"; import { setDirectionAction } from "viewer/model/actions/flycam_actions"; import { diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated.ts b/frontend/javascripts/viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated.ts index 71ae1ddb488..b2b6cb25f61 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/prefetch_strategy_plane_rotated.ts @@ -1,4 +1,6 @@ +import { V3 } from "libs/mjs"; import _ from "lodash"; +import * as THREE from "three"; import type { AdditionalCoordinate } from "types/api_types"; import type { OrthoView, OrthoViewMap, Vector3, Vector4 } from "viewer/constants"; import constants, { OrthoViewValuesWithoutTDView } from "viewer/constants"; @@ -9,8 +11,6 @@ import type { PullQueueItem } from "viewer/model/bucket_data_handling/pullqueue" import Dimensions from "viewer/model/dimensions"; import { zoomedAddressToAnotherZoomStep } from "viewer/model/helpers/position_converter"; import type { MagInfo } from "../helpers/mag_info"; -import * as THREE from "three"; -import { V3 } from "libs/mjs"; const { MAX_ZOOM_STEP_DIFF_PREFETCH } = constants; diff --git a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts index 2105be7465e..d02f03ddd94 100644 --- a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts @@ -23,7 +23,6 @@ import { select } from "viewer/model/sagas/effect-generators"; import { Model } from "viewer/singletons"; import type { WebknossosState } from "viewer/store"; import { ensureWkReady } from "./ready_sagas"; -import _ from "lodash"; const PREFETCH_THROTTLE_TIME = 50; const DIRECTION_VECTOR_SMOOTHER = 0.125; diff --git a/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx b/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx index 83a0dcd04f8..936c33bf355 100644 --- a/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx +++ b/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx @@ -21,6 +21,7 @@ import ButtonComponent, { ToggleButton } from "viewer/view/components/button_com import FastTooltip from "components/fast_tooltip"; import features from "features"; +import { getDisabledInfoForTools } from "viewer/model/accessors/disabled_tool_accessor"; import { ChangeBrushSizePopover } from "./brush_presets"; import { SkeletonSpecificButtons } from "./skeleton_specific_ui"; import { ToolIdToComponent } from "./tool_buttons"; @@ -37,7 +38,6 @@ import { QuickSelectSettingsPopover, VolumeInterpolationButton, } from "./volume_specific_ui"; -import { getDisabledInfoForTools } from "viewer/model/accessors/disabled_tool_accessor"; const handleAddNewUserBoundingBox = () => { Store.dispatch(addUserBoundingBoxAction()); diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index 0653d4cfcad..8fa7712012a 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -62,6 +62,7 @@ import { getVisibleSegmentationLayer, } from "viewer/model/accessors/dataset_accessor"; import { getDisabledInfoForTools } from "viewer/model/accessors/disabled_tool_accessor"; +import { isRotated } from "viewer/model/accessors/flycam_accessor"; import { areGeometriesTransformed, getActiveNode, @@ -146,8 +147,6 @@ import { withMappingActivationConfirmation, } from "viewer/view/right-border-tabs/segments_tab/segments_view_helper"; import { LoadMeshMenuItemLabel } from "./right-border-tabs/segments_tab/load_mesh_menu_item_label"; -import { isRotated } from "viewer/model/accessors/flycam_accessor"; -import _ from "lodash"; type ContextMenuContextValue = React.MutableRefObject | null; export const ContextMenuContext = createContext(null); From bf9a1d3905d4d81f49a290ca9b0ec7b33cdfb7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 21 May 2025 13:23:14 +0200 Subject: [PATCH 046/128] fix ts errors --- frontend/javascripts/test/shaders/shader_syntax.spec.ts | 6 ++++++ .../viewer/view/action-bar/tools/toolbar_view.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/test/shaders/shader_syntax.spec.ts b/frontend/javascripts/test/shaders/shader_syntax.spec.ts index 76094555aa1..27e67e3cd25 100644 --- a/frontend/javascripts/test/shaders/shader_syntax.spec.ts +++ b/frontend/javascripts/test/shaders/shader_syntax.spec.ts @@ -46,6 +46,7 @@ describe("Shader syntax", () => { segmentationLayerNames: [], magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], + voxelSizeFactorInverted: [1, 1, 1], isOrthogonal: true, tpsTransformPerLayer: {}, }); @@ -97,6 +98,7 @@ describe("Shader syntax", () => { segmentationLayerNames: ["segmentationLayer"], magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], + voxelSizeFactorInverted: [1, 1, 1], isOrthogonal: true, tpsTransformPerLayer: {}, }); @@ -141,6 +143,7 @@ describe("Shader syntax", () => { segmentationLayerNames: ["segmentationLayer"], magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], + voxelSizeFactorInverted: [1, 1, 1], isOrthogonal: true, tpsTransformPerLayer: {}, }); @@ -177,6 +180,7 @@ describe("Shader syntax", () => { segmentationLayerNames: [], magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], + voxelSizeFactorInverted: [1, 1, 1], isOrthogonal: false, tpsTransformPerLayer: {}, }); @@ -221,6 +225,7 @@ describe("Shader syntax", () => { segmentationLayerNames: ["segmentationLayer"], magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], + voxelSizeFactorInverted: [1, 1, 1], isOrthogonal: false, tpsTransformPerLayer: {}, }); @@ -256,6 +261,7 @@ describe("Shader syntax", () => { segmentationLayerNames: [], magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], + voxelSizeFactorInverted: [1, 1, 1], isOrthogonal: true, tpsTransformPerLayer: {}, }); diff --git a/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx b/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx index 936c33bf355..ef8ac9dc58a 100644 --- a/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx +++ b/frontend/javascripts/viewer/view/action-bar/tools/toolbar_view.tsx @@ -100,7 +100,7 @@ export default function ToolbarView() { return ( <> - + {Toolkits[toolkit].map((tool) => { const ToolButton = ToolIdToComponent[tool.id]; return ; From 6d1f468e01c3bf54f592746ccabbb3acefd34022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 21 May 2025 17:21:00 +0200 Subject: [PATCH 047/128] fix frontend tests --- .../test/controller/url_manager.spec.ts | 3 ++- .../test/fixtures/volumetracing_object.ts | 6 +++++ .../test/reducers/flycam_reducer.spec.ts | 26 ++++++++++++++----- .../viewer/controller/url_manager.ts | 3 +++ .../viewer/model/reducers/flycam_reducer.ts | 25 ++++++++++++------ 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/frontend/javascripts/test/controller/url_manager.spec.ts b/frontend/javascripts/test/controller/url_manager.spec.ts index 8fc3696d7f4..227830b907f 100644 --- a/frontend/javascripts/test/controller/url_manager.spec.ts +++ b/frontend/javascripts/test/controller/url_manager.spec.ts @@ -209,7 +209,8 @@ describe("UrlManager", () => { it("should build default url in csv format", () => { UrlManager.initialize(); const url = UrlManager.buildUrl(); - expect(url).toBe("#0,0,0,0,1.3"); + // TODOM: Investigate why the rotation of z is 180. + expect(url).toBe("#0,0,0,0,1.3,0,0,180"); }); it("The dataset name should be correctly extracted from view URLs", () => { diff --git a/frontend/javascripts/test/fixtures/volumetracing_object.ts b/frontend/javascripts/test/fixtures/volumetracing_object.ts index d0b325cfafd..a97c3272e95 100644 --- a/frontend/javascripts/test/fixtures/volumetracing_object.ts +++ b/frontend/javascripts/test/fixtures/volumetracing_object.ts @@ -2,6 +2,7 @@ import update from "immutability-helper"; import { AnnotationTool } from "viewer/model/accessors/tool_accessor"; import Constants from "viewer/constants"; import defaultState from "viewer/default_state"; +import { M4x4 } from "libs/mjs"; const volumeTracing = { type: "volume", @@ -105,4 +106,9 @@ export const initialState = update(defaultState, { }, }, }, + flycam: { + currentMatrix: { + $set: M4x4.rotate(Math.PI, [0, 0, 1], M4x4.identity(), []), + }, + }, }); diff --git a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts index 025f2607212..d71dc7c425e 100644 --- a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts @@ -43,6 +43,13 @@ const initialState = { viewMode: "oblique", }, }; +const initialStateWithNoConceptualRotation = { + ...initialState, + flycam: { + ...initialState.flycam, + currentMatrix: M4x4.rotate(Math.PI, [0, 0, 1], M4x4.identity(), []), + }, +}; describe("Flycam", () => { it("should calculate zoomed matrix", () => { @@ -131,12 +138,16 @@ describe("Flycam", () => { it("should move in ortho mode", () => { const moveAction = FlycamActions.moveFlycamOrthoAction([2, 0, 0], OrthoViews.PLANE_XY); - const newState = FlycamReducer(initialState, moveAction); + const newState = FlycamReducer(initialStateWithNoConceptualRotation, moveAction); + console.error(newState.flycam); equalWithEpsilon(getPosition(newState.flycam), [2, 0, 0]); }); it("should move in ortho mode with dynamicSpaceDirection", () => { - let newState = FlycamReducer(initialState, FlycamActions.setDirectionAction([0, 0, -2])); + let newState = FlycamReducer( + initialStateWithNoConceptualRotation, + FlycamActions.setDirectionAction([0, 0, -2]), + ); newState = FlycamReducer( newState, FlycamActions.moveFlycamOrthoAction([2, 0, 2], OrthoViews.PLANE_XY), @@ -150,7 +161,7 @@ describe("Flycam", () => { OrthoViews.PLANE_XY, true, ); - const newState = FlycamReducer(initialState, moveAction); + const newState = FlycamReducer(initialStateWithNoConceptualRotation, moveAction); expect(getPosition(newState.flycam)).toEqual([4, 0, 0]); }); @@ -160,7 +171,7 @@ describe("Flycam", () => { OrthoViews.PLANE_XZ, true, ); - const newState = FlycamReducer(initialState, moveAction); + const newState = FlycamReducer(initialStateWithNoConceptualRotation, moveAction); expect(getPosition(newState.flycam)).toEqual([4, 0, 2]); }); @@ -170,12 +181,15 @@ describe("Flycam", () => { OrthoViews.PLANE_XZ, false, ); - const newState = FlycamReducer(initialState, moveAction); + const newState = FlycamReducer(initialStateWithNoConceptualRotation, moveAction); expect(getPosition(newState.flycam)).toEqual([2, 0, 1]); }); it("should move by plane in ortho mode with dynamicSpaceDirection", () => { - let newState = FlycamReducer(initialState, FlycamActions.setDirectionAction([0, 0, -2])); + let newState = FlycamReducer( + initialStateWithNoConceptualRotation, + FlycamActions.setDirectionAction([0, 0, -2]), + ); newState = FlycamReducer( newState, FlycamActions.movePlaneFlycamOrthoAction([0, 0, 2], OrthoViews.PLANE_XY, true), diff --git a/frontend/javascripts/viewer/controller/url_manager.ts b/frontend/javascripts/viewer/controller/url_manager.ts index 50bddf1d498..87c4babf685 100644 --- a/frontend/javascripts/viewer/controller/url_manager.ts +++ b/frontend/javascripts/viewer/controller/url_manager.ts @@ -167,6 +167,7 @@ class UrlManager { parseUrlHash(): PartialUrlManagerState { const urlHash = decodeURIComponent(location.hash.slice(1)); + console.error("Parsing URL hash:", urlHash); if (urlHash.includes("{")) { // The hash is in json format @@ -287,6 +288,7 @@ class UrlManager { const rotation = { rotation: Utils.map3((e) => Utils.roundTo(e, 2), getRotationInDegrees(state.flycam)), }; + console.error("rotation:", rotation, getRotationInDegrees(state.flycam)); const activeNode = state.annotation.skeleton?.activeNodeId; const activeNodeOptional = activeNode != null ? { activeNode } : {}; const stateByLayer: UrlStateByLayer = {}; @@ -404,6 +406,7 @@ class UrlManager { buildUrl(): string { const state = Store.getState(); + console.error("Building URL hash:", state.flycam); const hash = this.buildUrlHashCsv(state); const newBaseUrl = updateTypeAndId( this.baseUrl, diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 7edba4c21b0..50858c384b4 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -1,6 +1,6 @@ import update from "immutability-helper"; import type { Matrix4x4 } from "libs/mjs"; -import { M4x4 } from "libs/mjs"; +import { M4x4, V3 } from "libs/mjs"; import * as Utils from "libs/utils"; import _ from "lodash"; import * as THREE from "three"; @@ -309,16 +309,20 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler( new THREE.Euler(...flycamRotation), ); - const movementVectorInWorld = new THREE.Vector3(...vector).applyMatrix4(rotationMatrix); + let movementVectorInWorld = new THREE.Vector3(...vector) + .applyMatrix4(rotationMatrix) + .toArray(); // if planeID is given, use it to manipulate z if (planeId != null && state.userConfiguration.dynamicSpaceDirection) { // change direction of the value connected to space, based on the last direction - const dim = Dimensions.getIndices(planeId)[2]; - vector[dim] *= state.flycam.spaceDirectionOrtho[dim]; + movementVectorInWorld = V3.multiply( + movementVectorInWorld, + state.flycam.spaceDirectionOrtho, + ); } - return moveReducer(state, movementVectorInWorld.toArray()); + return moveReducer(state, movementVectorInWorld); } case "MOVE_PLANE_FLYCAM_ORTHO": { @@ -335,18 +339,23 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState const movementVectorInWorld = new THREE.Vector3(...vector).applyMatrix4(rotationMatrix); const zoomFactor = increaseSpeedWithZoom ? flycam.zoomStep : 1; const scaleFactor = getBaseVoxelFactorsInUnit(dataset.dataSource.scale); - const movementInWorldZoomed = movementVectorInWorld + let movementInWorldZoomed = movementVectorInWorld .multiplyScalar(zoomFactor) - .multiply(new THREE.Vector3(...scaleFactor)); + .multiply(new THREE.Vector3(...scaleFactor)) + .toArray(); // TODOM: make this apply to movementInWorldZoomed if (planeId != null && state.userConfiguration.dynamicSpaceDirection) { // change direction of the value connected to space, based on the last direction + movementInWorldZoomed = V3.multiply( + movementInWorldZoomed, + state.flycam.spaceDirectionOrtho, + ); // const dim = Dimensions.getIndices(planeId)[2]; // delta[dim] *= state.flycam.spaceDirectionOrtho[dim]; } - return moveReducer(state, movementInWorldZoomed.toArray()); + return moveReducer(state, movementInWorldZoomed); } return state; From 66d8ed648ef8a8d8b1fbbd28d54bab574b6eb70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 22 May 2025 10:15:48 +0200 Subject: [PATCH 048/128] remove debug logging --- frontend/javascripts/viewer/controller/url_manager.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/javascripts/viewer/controller/url_manager.ts b/frontend/javascripts/viewer/controller/url_manager.ts index 87c4babf685..50bddf1d498 100644 --- a/frontend/javascripts/viewer/controller/url_manager.ts +++ b/frontend/javascripts/viewer/controller/url_manager.ts @@ -167,7 +167,6 @@ class UrlManager { parseUrlHash(): PartialUrlManagerState { const urlHash = decodeURIComponent(location.hash.slice(1)); - console.error("Parsing URL hash:", urlHash); if (urlHash.includes("{")) { // The hash is in json format @@ -288,7 +287,6 @@ class UrlManager { const rotation = { rotation: Utils.map3((e) => Utils.roundTo(e, 2), getRotationInDegrees(state.flycam)), }; - console.error("rotation:", rotation, getRotationInDegrees(state.flycam)); const activeNode = state.annotation.skeleton?.activeNodeId; const activeNodeOptional = activeNode != null ? { activeNode } : {}; const stateByLayer: UrlStateByLayer = {}; @@ -406,7 +404,6 @@ class UrlManager { buildUrl(): string { const state = Store.getState(); - console.error("Building URL hash:", state.flycam); const hash = this.buildUrlHashCsv(state); const newBaseUrl = updateTypeAndId( this.baseUrl, From 4741cda8f32552ad02ef62260b0a9f04cb48ebbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 22 May 2025 14:38:02 +0200 Subject: [PATCH 049/128] fix create node api --- frontend/javascripts/viewer/api/api_latest.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index d3ff6b1ecab..3732667a2f4 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -379,8 +379,9 @@ class TracingApi { skipCenteringAnimationInThirdDimension?: boolean; }, ) { - const globalPosition = - "rounded" in position ? position : { rounded: position, floating: position }; + const globalPosition = Array.isArray(position) + ? { rounded: Utils.map3(Math.round, position), floating: position } + : position; assertSkeleton(Store.getState().annotation); const defaultOptions = getOptionsForCreateSkeletonNode(); createSkeletonNode( From 848ed1f223db6c47052c9e8118d2dfa7a1c2a38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 23 May 2025 13:08:51 +0200 Subject: [PATCH 050/128] fix bucket picking by fixing applying flycam rotation to camera & objects --- .../viewer/controller/camera_controller.ts | 6 ++--- .../combinations/skeleton_handlers.ts | 4 +-- .../viewer/controller/scene_controller.ts | 6 +++-- .../model/accessors/disabled_tool_accessor.ts | 8 ++---- .../viewer/model/accessors/flycam_accessor.ts | 26 ++++++++----------- .../model/accessors/view_mode_accessor.ts | 2 +- .../viewer/model/reducers/flycam_reducer.ts | 4 +-- 7 files changed, 25 insertions(+), 31 deletions(-) diff --git a/frontend/javascripts/viewer/controller/camera_controller.ts b/frontend/javascripts/viewer/controller/camera_controller.ts index fb4007f2e20..ce55c86d55f 100644 --- a/frontend/javascripts/viewer/controller/camera_controller.ts +++ b/frontend/javascripts/viewer/controller/camera_controller.ts @@ -163,13 +163,13 @@ class CameraController extends React.PureComponent { const gRot = getRotationInRadian(state.flycam); // Copies are needed because multiply modifies the matrix in-place. const rotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler( - new THREE.Euler(gRot[0], gRot[1], gRot[2]), + new THREE.Euler(gRot[0], gRot[1], gRot[2], "ZYX"), ); const rotationMatrixYZ = new THREE.Matrix4().makeRotationFromEuler( - new THREE.Euler(gRot[0], gRot[1], gRot[2]), + new THREE.Euler(gRot[0], gRot[1], gRot[2], "ZYX"), ); const rotationMatrixXZ = new THREE.Matrix4().makeRotationFromEuler( - new THREE.Euler(gRot[0], gRot[1], gRot[2]), + new THREE.Euler(gRot[0], gRot[1], gRot[2], "ZYX"), ); const baseRotationMatrixXY = new THREE.Matrix4().makeRotationFromEuler( OrthoBaseRotations[OrthoViews.PLANE_XY], diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index fdd217ebe55..a9f6464eb60 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -199,7 +199,7 @@ export function moveNode( const isRotated = V3.equals(flycamRotation, [0, 0, 0]); const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler( - new THREE.Euler(...flycamRotation), + new THREE.Euler(...flycamRotation, "ZYX"), ); const vectorRotated = new THREE.Vector3(...vector).applyMatrix4(rotationMatrix); @@ -302,7 +302,7 @@ export function getOptionsForCreateSkeletonNode( const flycamRotation = getRotationInRadian(state.flycam); const totalRotationQuaternion = new THREE.Quaternion() .setFromEuler(new THREE.Euler(...initialViewportRotation)) - .multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(...flycamRotation))); + .multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(...flycamRotation, "ZYX"))); const rotationEuler = new THREE.Euler().setFromQuaternion(totalRotationQuaternion); const rotationInDegree = [rotationEuler.x, rotationEuler.y, rotationEuler.z].map( (a) => (a * 180) / Math.PI, diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index 450757e34c7..f484cbf3325 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -431,10 +431,12 @@ class SceneController { : Math.floor(-this.clippingDistance); const rotatedPositionOffsetVector = new THREE.Vector3( ...unrotatedPositionOffset, - ).applyEuler(new THREE.Euler(...rotation)); + ).applyEuler(new THREE.Euler(...rotation, "ZYX")); const rotatedPositionOffset = rotatedPositionOffsetVector.toArray(); this.planes[planeId].setPosition(originalPosition, rotatedPositionOffset); - this.planes[planeId].setRotation(new THREE.Euler(rotation[0], rotation[1], rotation[2])); + this.planes[planeId].setRotation( + new THREE.Euler(rotation[0], rotation[1], rotation[2], "ZYX"), + ); this.quickSelectGeometry.adaptVisibilityForRendering(originalPosition, ind[2]); } else { diff --git a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts index ef24ba676c0..8cf7c7baa9d 100644 --- a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts @@ -7,11 +7,7 @@ import memoizeOne from "memoize-one"; import type { APIOrganization, APIUser } from "types/api_types"; import { IdentityTransform } from "viewer/constants"; import { getVisibleSegmentationLayer } from "viewer/model/accessors/dataset_accessor"; -import { - getRotationInRadian, - isMagRestrictionViolated, - isRotated, -} from "viewer/model/accessors/flycam_accessor"; +import { isMagRestrictionViolated, isRotated } from "viewer/model/accessors/flycam_accessor"; import type { WebknossosState } from "viewer/store"; import { reuseInstanceOnEquality } from "./accessor_helpers"; import { getTransformsPerLayer } from "./dataset_layer_transformation_accessor"; @@ -332,7 +328,7 @@ function getDisabledVolumeInfo(state: WebknossosState) { const { activeMappingByLayer } = state.temporaryConfiguration; const isZoomInvalidForTracing = isMagRestrictionViolated(state); const hasVolume = state.annotation.volumes.length > 0; - const isFlycamRotated = !_.isEqual(getRotationInRadian(state.flycam), [0, 0, 0]); + const isFlycamRotated = isRotated(state.flycam); const hasSkeleton = state.annotation.skeleton != null; const segmentationTracingLayer = getActiveSegmentationTracing(state); const labeledMag = getRenderableMagForSegmentationTracing(state, segmentationTracingLayer)?.mag; diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index 9abc2b9403a..8b7e86e17e4 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -297,23 +297,20 @@ function _getFlooredPosition(flycam: Flycam): Vector3 { return map3((x) => Math.floor(x), _getPosition(flycam)); } -function _getRotationInRadianFixed(flycam: Flycam): Vector3 { +// Returns the current rotation of the flycam in radians as an euler xyz tuple. +// As the order in which the angles are applied is zyx (see flycam_reducer), +// this order must be followed when this euler angle is applied to 2d computations. +function _getRotationInRadian(flycam: Flycam, invertZ: boolean = true): Vector3 { + // Somehow z rotation is inverted but the others are not. + const zInvertFactor = invertZ ? -1 : 1; const object = new THREE.Object3D(); const matrix = new THREE.Matrix4().fromArray(flycam.currentMatrix).transpose(); object.applyMatrix4(matrix); - const rotation: Vector3 = [object.rotation.x, object.rotation.y - Math.PI, object.rotation.z]; - return [ - mod(rotation[0], Math.PI * 2), - mod(rotation[1], Math.PI * 2), - mod(rotation[2], Math.PI * 2), + const rotation: Vector3 = [ + object.rotation.x, + object.rotation.y, + (object.rotation.z - Math.PI) * zInvertFactor, ]; -} - -function _getRotationInRadian(flycam: Flycam): Vector3 { - const object = new THREE.Object3D(); - const matrix = new THREE.Matrix4().fromArray(flycam.currentMatrix).transpose(); - object.applyMatrix4(matrix); - const rotation: Vector3 = [object.rotation.x, object.rotation.y, object.rotation.z - Math.PI]; return [ mod(rotation[0], Math.PI * 2), mod(rotation[1], Math.PI * 2), @@ -322,7 +319,7 @@ function _getRotationInRadian(flycam: Flycam): Vector3 { } function _getRotationInDegrees(flycam: Flycam): Vector3 { - const rotationInRadian = getRotationInRadian(flycam); + const rotationInRadian = getRotationInRadian(flycam, false); // Modulo operation not needed as already done in getRotationInRadian. return [ (180 / Math.PI) * rotationInRadian[0], @@ -342,7 +339,6 @@ export const getUp = memoizeOne(_getUp); export const getLeft = memoizeOne(_getLeft); export const getPosition = memoizeOne(_getPosition); export const getFlooredPosition = memoizeOne(_getFlooredPosition); -export const getRotationInRadianFixed = memoizeOne(_getRotationInRadianFixed); export const getRotationInRadian = memoizeOne(_getRotationInRadian); export const getRotationInDegrees = memoizeOne(_getRotationInDegrees); export const isRotated = memoizeOne(_isRotated); diff --git a/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts b/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts index b33de246012..990b590524b 100644 --- a/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts @@ -112,7 +112,7 @@ function _calculateMaybeGlobalPos( const positionInPlane = [diffX, diffY, 0] as Vector3; const positionPlaneDefaultRotation = Dimensions.transDim(positionInPlane, planeIdFilled); const flycamRotationMatrix = new THREE.Matrix4().makeRotationFromEuler( - new THREE.Euler(...flycamRotation), + new THREE.Euler(...flycamRotation, "ZYX"), ); const flycamPositionMatrix = new THREE.Matrix4().makeTranslation( new THREE.Vector3(...curGlobalPos), diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 50858c384b4..69207a4c012 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -307,7 +307,7 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState const flycamRotation = getRotationInRadian(state.flycam); const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler( - new THREE.Euler(...flycamRotation), + new THREE.Euler(...flycamRotation, "ZYX"), ); let movementVectorInWorld = new THREE.Vector3(...vector) .applyMatrix4(rotationMatrix) @@ -334,7 +334,7 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState const flycamRotation = getRotationInRadian(flycam); const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler( - new THREE.Euler(...flycamRotation), + new THREE.Euler(...flycamRotation, "ZYX"), ); const movementVectorInWorld = new THREE.Vector3(...vector).applyMatrix4(rotationMatrix); const zoomFactor = increaseSpeedWithZoom ? flycam.zoomStep : 1; From 01274137007cf3bd1396e167c22995a8e4b3a392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 23 May 2025 14:42:33 +0200 Subject: [PATCH 051/128] clean up --- .../test/controller/url_manager.spec.ts | 2 +- .../test/fixtures/volumetracing_object.ts | 2 ++ .../test/reducers/flycam_reducer.spec.ts | 17 +++++++++-------- .../test/shaders/shader_syntax.spec.ts | 6 ------ frontend/javascripts/viewer/api/api_latest.ts | 1 - .../viewer/geometries/arbitrary_plane.ts | 2 +- .../materials/plane_material_factory.ts | 6 +----- frontend/javascripts/viewer/geometries/plane.ts | 9 ++++----- .../model/accessors/disabled_tool_accessor.ts | 1 - .../viewer/model/accessors/flycam_accessor.ts | 4 +++- .../model/helpers/action_logger_middleware.ts | 2 +- .../viewer/model/sagas/prefetch_saga.ts | 2 +- .../viewer/model/sagas/volumetracing_saga.tsx | 1 - .../viewer/shaders/filtering.glsl.ts | 6 +----- .../viewer/shaders/main_data_shaders.glsl.ts | 1 - 15 files changed, 24 insertions(+), 38 deletions(-) diff --git a/frontend/javascripts/test/controller/url_manager.spec.ts b/frontend/javascripts/test/controller/url_manager.spec.ts index 227830b907f..c27235810fe 100644 --- a/frontend/javascripts/test/controller/url_manager.spec.ts +++ b/frontend/javascripts/test/controller/url_manager.spec.ts @@ -209,7 +209,7 @@ describe("UrlManager", () => { it("should build default url in csv format", () => { UrlManager.initialize(); const url = UrlManager.buildUrl(); - // TODOM: Investigate why the rotation of z is 180. + // There is a default rotation of 180 around z which needs to be accounted for here. expect(url).toBe("#0,0,0,0,1.3,0,0,180"); }); diff --git a/frontend/javascripts/test/fixtures/volumetracing_object.ts b/frontend/javascripts/test/fixtures/volumetracing_object.ts index a97c3272e95..1cbc9b3411b 100644 --- a/frontend/javascripts/test/fixtures/volumetracing_object.ts +++ b/frontend/javascripts/test/fixtures/volumetracing_object.ts @@ -108,6 +108,8 @@ export const initialState = update(defaultState, { }, flycam: { currentMatrix: { + // Apply the default 180 z axis rotation to get correct result in ortho related tests. + // This makes the calculated flycam rotation to [0, 0, 0]. Otherwise it would be [0, 0, 180]. $set: M4x4.rotate(Math.PI, [0, 0, 1], M4x4.identity(), []), }, }, diff --git a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts index d71dc7c425e..f3797843a67 100644 --- a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts @@ -43,10 +43,12 @@ const initialState = { viewMode: "oblique", }, }; -const initialStateWithNoConceptualRotation = { +const initialStateWithDefaultRotation = { ...initialState, flycam: { ...initialState.flycam, + // Apply the default 180 z axis rotation to get correct result in ortho related tests. + // This makes the calculated flycam rotation to [0, 0, 0]. Otherwise it would be [0, 0, 180]. currentMatrix: M4x4.rotate(Math.PI, [0, 0, 1], M4x4.identity(), []), }, }; @@ -138,14 +140,13 @@ describe("Flycam", () => { it("should move in ortho mode", () => { const moveAction = FlycamActions.moveFlycamOrthoAction([2, 0, 0], OrthoViews.PLANE_XY); - const newState = FlycamReducer(initialStateWithNoConceptualRotation, moveAction); - console.error(newState.flycam); + const newState = FlycamReducer(initialStateWithDefaultRotation, moveAction); equalWithEpsilon(getPosition(newState.flycam), [2, 0, 0]); }); it("should move in ortho mode with dynamicSpaceDirection", () => { let newState = FlycamReducer( - initialStateWithNoConceptualRotation, + initialStateWithDefaultRotation, FlycamActions.setDirectionAction([0, 0, -2]), ); newState = FlycamReducer( @@ -161,7 +162,7 @@ describe("Flycam", () => { OrthoViews.PLANE_XY, true, ); - const newState = FlycamReducer(initialStateWithNoConceptualRotation, moveAction); + const newState = FlycamReducer(initialStateWithDefaultRotation, moveAction); expect(getPosition(newState.flycam)).toEqual([4, 0, 0]); }); @@ -171,7 +172,7 @@ describe("Flycam", () => { OrthoViews.PLANE_XZ, true, ); - const newState = FlycamReducer(initialStateWithNoConceptualRotation, moveAction); + const newState = FlycamReducer(initialStateWithDefaultRotation, moveAction); expect(getPosition(newState.flycam)).toEqual([4, 0, 2]); }); @@ -181,13 +182,13 @@ describe("Flycam", () => { OrthoViews.PLANE_XZ, false, ); - const newState = FlycamReducer(initialStateWithNoConceptualRotation, moveAction); + const newState = FlycamReducer(initialStateWithDefaultRotation, moveAction); expect(getPosition(newState.flycam)).toEqual([2, 0, 1]); }); it("should move by plane in ortho mode with dynamicSpaceDirection", () => { let newState = FlycamReducer( - initialStateWithNoConceptualRotation, + initialStateWithDefaultRotation, FlycamActions.setDirectionAction([0, 0, -2]), ); newState = FlycamReducer( diff --git a/frontend/javascripts/test/shaders/shader_syntax.spec.ts b/frontend/javascripts/test/shaders/shader_syntax.spec.ts index 27e67e3cd25..211ef07fc59 100644 --- a/frontend/javascripts/test/shaders/shader_syntax.spec.ts +++ b/frontend/javascripts/test/shaders/shader_syntax.spec.ts @@ -47,7 +47,6 @@ describe("Shader syntax", () => { magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], voxelSizeFactorInverted: [1, 1, 1], - isOrthogonal: true, tpsTransformPerLayer: {}, }); @@ -99,7 +98,6 @@ describe("Shader syntax", () => { magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], voxelSizeFactorInverted: [1, 1, 1], - isOrthogonal: true, tpsTransformPerLayer: {}, }); parser.parse(code); @@ -144,7 +142,6 @@ describe("Shader syntax", () => { magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], voxelSizeFactorInverted: [1, 1, 1], - isOrthogonal: true, tpsTransformPerLayer: {}, }); @@ -181,7 +178,6 @@ describe("Shader syntax", () => { magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], voxelSizeFactorInverted: [1, 1, 1], - isOrthogonal: false, tpsTransformPerLayer: {}, }); parser.parse(code); @@ -226,7 +222,6 @@ describe("Shader syntax", () => { magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], voxelSizeFactorInverted: [1, 1, 1], - isOrthogonal: false, tpsTransformPerLayer: {}, }); parser.parse(code); @@ -262,7 +257,6 @@ describe("Shader syntax", () => { magnificationsCount: mags.length, voxelSizeFactor: [1, 1, 1], voxelSizeFactorInverted: [1, 1, 1], - isOrthogonal: true, tpsTransformPerLayer: {}, }); parser.parse(code); diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index 3732667a2f4..3ea0c14b519 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -1420,7 +1420,6 @@ class TracingApi { const curPosition = getPosition(flycam); const curRotation = getRotationInDegrees(flycam); const isNotRotated = V3.equals(curRotation, [0, 0, 0]); - // TODOM: Fix this 3rd dimension calculation. Otherwise centering will lead to slowly moving along the 3rd dimension and thus not staying in the slice when rotation is active and not axis aligned. const dimensionToSkip = skipCenteringAnimationInThirdDimension && activeViewport !== OrthoViews.TDView && isNotRotated ? dimensions.thirdDimensionForPlane(activeViewport) diff --git a/frontend/javascripts/viewer/geometries/arbitrary_plane.ts b/frontend/javascripts/viewer/geometries/arbitrary_plane.ts index 305e6b39b37..bfac0cbb4db 100644 --- a/frontend/javascripts/viewer/geometries/arbitrary_plane.ts +++ b/frontend/javascripts/viewer/geometries/arbitrary_plane.ts @@ -101,7 +101,7 @@ class ArbitraryPlane { return _plane; }; - this.materialFactory = new PlaneMaterialFactory(OrthoViews.PLANE_XY, false, 4); + this.materialFactory = new PlaneMaterialFactory(OrthoViews.PLANE_XY, 4); const textureMaterial = this.materialFactory.setup().getMaterial(); const mainPlane = adaptPlane( new THREE.Mesh( diff --git a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts index d4b8fd1f6ea..184842f3cd7 100644 --- a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts +++ b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts @@ -112,7 +112,6 @@ function getTextureLayerInfos(): Params["textureLayerInfos"] { class PlaneMaterialFactory { planeID: OrthoView; - isOrthogonal: boolean; material: THREE.ShaderMaterial | undefined | null; uniforms: Uniforms = {}; attributes: Record = {}; @@ -126,9 +125,8 @@ class PlaneMaterialFactory { scaledTpsInvPerLayer: Record = {}; - constructor(planeID: OrthoView, isOrthogonal: boolean, shaderId: number) { + constructor(planeID: OrthoView, shaderId: number) { this.planeID = planeID; - this.isOrthogonal = isOrthogonal; this.shaderId = shaderId; this.leastRecentlyVisibleLayers = []; } @@ -1091,7 +1089,6 @@ class PlaneMaterialFactory { magnificationsCount: this.getTotalMagCount(), voxelSizeFactor, voxelSizeFactorInverted, - isOrthogonal: this.isOrthogonal, tpsTransformPerLayer: this.scaledTpsInvPerLayer, }); return [ @@ -1128,7 +1125,6 @@ class PlaneMaterialFactory { magnificationsCount: this.getTotalMagCount(), voxelSizeFactor, voxelSizeFactorInverted, - isOrthogonal: this.isOrthogonal, tpsTransformPerLayer: this.scaledTpsInvPerLayer, }); } diff --git a/frontend/javascripts/viewer/geometries/plane.ts b/frontend/javascripts/viewer/geometries/plane.ts index e2e59c49d45..1992093da35 100644 --- a/frontend/javascripts/viewer/geometries/plane.ts +++ b/frontend/javascripts/viewer/geometries/plane.ts @@ -70,12 +70,10 @@ class Plane { this.materialFactory = new PlaneMaterialFactory( this.planeID, - false, OrthoViewValues.indexOf(this.planeID), ); const textureMaterial = this.materialFactory.setup().getMaterial(); this.plane = new THREE.Mesh(planeGeo, textureMaterial); - this.plane.name = `${this.planeID}-plane`; // Create crosshairs this.crosshair = new Array(2); @@ -98,7 +96,6 @@ class Plane { // The default renderOrder is 0. In order for the crosshairs to be shown // render them AFTER the plane has been rendered. this.crosshair[i].renderOrder = 1; - this.crosshair[i].name = `${this.planeID}-crosshair-${i}`; } // Create borders @@ -115,7 +112,6 @@ class Plane { tdBorderGeometry, this.getLineBasicMaterial(OrthoViewColors[this.planeID], 1), ); - this.TDViewBorders.name = `${this.planeID}-TDViewBorders`; } setDisplayCrosshair = (value: boolean): void => { @@ -166,6 +162,7 @@ class Plane { }; setRotation = (rotVec: THREE.Euler): void => { + // rotVec must be in "ZYX" order as this is how the flycam operates (see flycam_reducer setRotationReducer) const baseRotationMatrix = new THREE.Matrix4().makeRotationFromEuler(this.baseRotation); const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(rotVec); const combinedMatrix = rotationMatrix.multiply(baseRotationMatrix); @@ -180,7 +177,9 @@ class Plane { originalPosition: Vector3, positionOffset: Vector3 = DEFAULT_POSITION_OFFSET, ): void => { - // TODOM: Write proper reasoning comment. + // As the world scaling by the dataset scale factor is inverted by the scene group + // containing all planes to avoid sheering in anisotropic scaled datasets. + // Thus, this scale needs to be applied manually to the position here. const scaledPosition = V3.multiply(originalPosition, this.datasetScaleFactor); // The offset is in screen space already so no scaling is necessary. const offsetPosition = V3.add(scaledPosition, positionOffset); diff --git a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts index 8cf7c7baa9d..2e825995e59 100644 --- a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts @@ -13,7 +13,6 @@ import { reuseInstanceOnEquality } from "./accessor_helpers"; import { getTransformsPerLayer } from "./dataset_layer_transformation_accessor"; import { areGeometriesTransformed, isSkeletonLayerVisible } from "./skeletontracing_accessor"; -import _ from "lodash"; import { type AgglomerateState, getActiveSegmentationTracing, diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index 8b7e86e17e4..6a3c0a96561 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -298,7 +298,7 @@ function _getFlooredPosition(flycam: Flycam): Vector3 { } // Returns the current rotation of the flycam in radians as an euler xyz tuple. -// As the order in which the angles are applied is zyx (see flycam_reducer), +// As the order in which the angles are applied is zyx (see flycam_reducer setRotationReducer), // this order must be followed when this euler angle is applied to 2d computations. function _getRotationInRadian(flycam: Flycam, invertZ: boolean = true): Vector3 { // Somehow z rotation is inverted but the others are not. @@ -518,6 +518,8 @@ export function getPlaneExtentInVoxel( const { width, height } = rects[planeID]; return [width * zoomStep, height * zoomStep]; } + +// TODOM: Investigate why these values are different to OrthoBaseRotations. export function getRotationOrthoInRadian(planeId: OrthoView): Vector3 { switch (planeId) { case OrthoViews.PLANE_YZ: diff --git a/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts b/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts index 0b1068aba1d..188930c22dc 100644 --- a/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts +++ b/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts @@ -20,7 +20,7 @@ const actionBlacklist = [ "SET_INPUT_CATCHER_RECT", "SET_MOUSE_POSITION", "SET_POSITION", - //"SET_ROTATION", + "SET_ROTATION", "SET_TD_CAMERA", "SET_VIEWPORT", "ZOOM_TD_VIEW", diff --git a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts index d02f03ddd94..d3394cc0a5a 100644 --- a/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/prefetch_saga.ts @@ -151,8 +151,8 @@ export function* prefetchForPlaneMode( if (WkDevFlags.bucketDebugging.visualizePrefetchedBuckets) { for (const item of buckets) { const bucket = layer.cube.getOrCreateBucket(item.bucket); + if (bucket.type !== "null") { - bucket.setVisualizationColor("green"); bucket.visualize(); } } diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index cb4708b2bb6..b449b1f4987 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -611,7 +611,6 @@ function* getGlobalMousePosition(): Saga { x, y, })?.rounded; - //TODOM: Might be better to use floating variant here for more accurate results, but maybe it is even more correct as the shader also enforces rounded positions. } return undefined; diff --git a/frontend/javascripts/viewer/shaders/filtering.glsl.ts b/frontend/javascripts/viewer/shaders/filtering.glsl.ts index 6de891705f7..e7feba07204 100644 --- a/frontend/javascripts/viewer/shaders/filtering.glsl.ts +++ b/frontend/javascripts/viewer/shaders/filtering.glsl.ts @@ -90,11 +90,7 @@ const getMaybeFilteredColor: ShaderModule = { ) { vec4 color; if (!suppressBilinearFiltering && useBilinearFiltering) { - <% if (isOrthogonal) { %> - color = getBilinearColorFor(layerIndex, d_texture_width, packingDegree, worldPositionUVW); - <% } else { %> - color = getTrilinearColorFor(layerIndex, d_texture_width, packingDegree, worldPositionUVW); - <% } %> + color = getTrilinearColorFor(layerIndex, d_texture_width, packingDegree, worldPositionUVW); } else { color = getColorForCoords(layerIndex, d_texture_width, packingDegree, worldPositionUVW, supportsPrecomputedBucketAddress); } diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index 60b7629daf2..a08f3931d11 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -59,7 +59,6 @@ export type Params = { magnificationsCount: number; voxelSizeFactor: Vector3; voxelSizeFactorInverted: Vector3; - isOrthogonal: boolean; tpsTransformPerLayer: Record; }; From dee20f016332469423041e785fdf88fe9f6b9f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 26 May 2025 17:46:13 +0200 Subject: [PATCH 052/128] add rotation popover --- .../view/action-bar/dataset_position_view.tsx | 237 +++++++----------- .../dataset_rotation_popover_view.tsx | 89 +++++++ .../viewer/view/action_bar_view.tsx | 4 +- .../view/components/setting_input_views.tsx | 9 +- 4 files changed, 194 insertions(+), 145 deletions(-) create mode 100644 frontend/javascripts/viewer/view/action-bar/dataset_rotation_popover_view.tsx diff --git a/frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx b/frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx index 49e3b70cc72..d1c7ca9b3e1 100644 --- a/frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx +++ b/frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx @@ -1,29 +1,23 @@ -import { PushpinOutlined, RollbackOutlined } from "@ant-design/icons"; +import { PushpinOutlined } from "@ant-design/icons"; import { Space } from "antd"; import FastTooltip from "components/fast_tooltip"; import { V3 } from "libs/mjs"; +import { useWkSelector } from "libs/react_hooks"; import Toast from "libs/toast"; import { Vector3Input } from "libs/vector_input"; import message from "messages"; import type React from "react"; -import { PureComponent } from "react"; -import { connect } from "react-redux"; -import type { APIDataset } from "types/api_types"; -import type { Vector3, ViewMode } from "viewer/constants"; +import { useCallback, useMemo } from "react"; +import type { EmptyObject } from "types/globals"; +import type { Vector3 } from "viewer/constants"; import { getDatasetExtentInVoxel } from "viewer/model/accessors/dataset_accessor"; -import { getPosition, getRotationInDegrees } from "viewer/model/accessors/flycam_accessor"; -import { setPositionAction, setRotationAction } from "viewer/model/actions/flycam_actions"; -import type { Flycam, Task, WebknossosState } from "viewer/store"; +import { getPosition } from "viewer/model/accessors/flycam_accessor"; +import { setPositionAction } from "viewer/model/actions/flycam_actions"; import Store from "viewer/store"; import { ShareButton } from "viewer/view/action-bar/share_modal_view"; import ButtonComponent from "viewer/view/components/button_component"; +import DatasetRotationPopoverButtonView from "./dataset_rotation_popover_view"; -type Props = { - flycam: Flycam; - viewMode: ViewMode; - dataset: APIDataset; - task: Task | null | undefined; -}; const positionIconStyle: React.CSSProperties = { transform: "rotate(-45deg)", marginRight: 0, @@ -41,146 +35,107 @@ const positionInputErrorStyle: React.CSSProperties = { ...warningColors, }; -class DatasetPositionView extends PureComponent { - copyPositionToClipboard = async () => { - const position = V3.floor(getPosition(this.props.flycam)).join(", "); - await navigator.clipboard.writeText(position); - Toast.success("Position copied to clipboard"); - }; +const DatasetPositionAndRotationView: React.FC = () => { + const flycam = useWkSelector((state) => state.flycam); + const dataset = useWkSelector((state) => state.dataset); + const task = useWkSelector((state) => state.task); - handleChangePosition = (position: Vector3) => { - Store.dispatch(setPositionAction(position)); - }; + const position = useMemo(() => V3.floor(getPosition(flycam)), [flycam]); - handleChangeRotation = (rotation: Vector3) => { - Store.dispatch(setRotationAction(rotation)); - }; + const isPositionOutOfBounds = useCallback( + (position: Vector3) => { + const { min: datasetMin, max: datasetMax } = getDatasetExtentInVoxel(dataset); - isPositionOutOfBounds = (position: Vector3) => { - const { dataset, task } = this.props; - const { min: datasetMin, max: datasetMax } = getDatasetExtentInVoxel(dataset); + const isPositionOutOfBounds = (min: Vector3, max: Vector3) => + position[0] < min[0] || + position[1] < min[1] || + position[2] < min[2] || + position[0] >= max[0] || + position[1] >= max[1] || + position[2] >= max[2]; - const isPositionOutOfBounds = (min: Vector3, max: Vector3) => - position[0] < min[0] || - position[1] < min[1] || - position[2] < min[2] || - position[0] >= max[0] || - position[1] >= max[1] || - position[2] >= max[2]; + const isOutOfDatasetBounds = isPositionOutOfBounds(datasetMin, datasetMax); + let isOutOfTaskBounds = false; - const isOutOfDatasetBounds = isPositionOutOfBounds(datasetMin, datasetMax); - let isOutOfTaskBounds = false; + if (task?.boundingBox) { + const bbox = task.boundingBox; + const bboxMax = [ + bbox.topLeft[0] + bbox.width, + bbox.topLeft[1] + bbox.height, + bbox.topLeft[2] + bbox.depth, + ]; + // @ts-expect-error ts-migrate(2345) + isOutOfTaskBounds = isPositionOutOfBounds(bbox.topLeft, bboxMax); + } - if (task?.boundingBox) { - const bbox = task.boundingBox; - const bboxMax = [ - bbox.topLeft[0] + bbox.width, - bbox.topLeft[1] + bbox.height, - bbox.topLeft[2] + bbox.depth, - ]; - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'number[]' is not assignable to p... Remove this comment to see the full error message - isOutOfTaskBounds = isPositionOutOfBounds(bbox.topLeft, bboxMax); - } + return { + isOutOfDatasetBounds, + isOutOfTaskBounds, + }; + }, + [dataset, task], + ); - return { - isOutOfDatasetBounds, - isOutOfTaskBounds, - }; - }; + const { isOutOfDatasetBounds, isOutOfTaskBounds } = isPositionOutOfBounds(position); + const iconColoringStyle = isOutOfDatasetBounds || isOutOfTaskBounds ? iconErrorStyle : {}; + const positionInputStyle = + isOutOfDatasetBounds || isOutOfTaskBounds ? positionInputErrorStyle : positionInputDefaultStyle; - render() { - const position = V3.floor(getPosition(this.props.flycam)); - const { isOutOfDatasetBounds, isOutOfTaskBounds } = this.isPositionOutOfBounds(position); - const iconColoringStyle = isOutOfDatasetBounds || isOutOfTaskBounds ? iconErrorStyle : {}; - const positionInputStyle = - isOutOfDatasetBounds || isOutOfTaskBounds - ? positionInputErrorStyle - : positionInputDefaultStyle; - let maybeErrorMessage = null; + let maybeErrorMessage = null; + if (isOutOfDatasetBounds) { + maybeErrorMessage = message["tracing.out_of_dataset_bounds"]; + } else if (!maybeErrorMessage && isOutOfTaskBounds) { + maybeErrorMessage = message["tracing.out_of_task_bounds"]; + } - if (isOutOfDatasetBounds) { - maybeErrorMessage = message["tracing.out_of_dataset_bounds"]; - } else if (!maybeErrorMessage && isOutOfTaskBounds) { - maybeErrorMessage = message["tracing.out_of_task_bounds"]; - } + const copyPositionToClipboard = useCallback(async () => { + const posStr = V3.floor(getPosition(flycam)).join(", "); + await navigator.clipboard.writeText(posStr); + Toast.success("Position copied to clipboard"); + }, [flycam]); - const rotation = V3.round(getRotationInDegrees(this.props.flycam)); - const positionView = ( -
{ + Store.dispatch(setPositionAction(position)); + }, []); + + const positionView = ( +
+ - - - - - - - - - - { - + - - this.handleChangeRotation([0, 0, 0])} - style={{ - padding: "0 10px", - }} - className="hide-on-small-screen" - > - - - - - - } -
- ); - return ( - - {positionView} - - ); - } -} + + + + + + + +
+ ); -function mapStateToProps(state: WebknossosState): Props { - return { - flycam: state.flycam, - viewMode: state.temporaryConfiguration.viewMode, - dataset: state.dataset, - task: state.task, - }; -} + return ( + + {positionView} + + ); +}; -const connector = connect(mapStateToProps); -export default connector(DatasetPositionView); +export default DatasetPositionAndRotationView; diff --git a/frontend/javascripts/viewer/view/action-bar/dataset_rotation_popover_view.tsx b/frontend/javascripts/viewer/view/action-bar/dataset_rotation_popover_view.tsx new file mode 100644 index 00000000000..8028af10428 --- /dev/null +++ b/frontend/javascripts/viewer/view/action-bar/dataset_rotation_popover_view.tsx @@ -0,0 +1,89 @@ +import { RollbackOutlined, SyncOutlined } from "@ant-design/icons"; +import { Button, Popover } from "antd"; +import { V3 } from "libs/mjs"; +import { useWkSelector } from "libs/react_hooks"; +import type React from "react"; +import { useCallback, useMemo } from "react"; +import type { EmptyObject } from "types/globals"; +import type { Vector3 } from "viewer/constants"; +import { getRotationInDegrees } from "viewer/model/accessors/flycam_accessor"; +import { setRotationAction } from "viewer/model/actions/flycam_actions"; +import Store from "viewer/store"; +import { NumberSliderSetting } from "../components/setting_input_views"; + +const DatasetRotationPopoverButtonView: React.FC = () => { + const flycam = useWkSelector((state) => state.flycam); + const rotation = useMemo(() => V3.round(getRotationInDegrees(flycam)), [flycam]); + + const handleChangeRotation = useCallback((rotation: Vector3) => { + Store.dispatch(setRotationAction(rotation)); + }, []); + + return ( + +
+ handleChangeRotation([newValue, rotation[1], rotation[2]])} + spans={[3, 13, 4, 4]} + postComponent={ +
+
+ handleChangeRotation([rotation[0], newValue, rotation[2]])} + spans={[3, 13, 4, 4]} + postComponent={ +
+
+ handleChangeRotation([rotation[0], rotation[1], newValue])} + spans={[3, 13, 4, 4]} + postComponent={ +
+
+ } + > + + + + ); }; From c0acca5d8c15d87ae82b5f5e98bfe618682c08d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:38:33 +0200 Subject: [PATCH 089/128] WIP: add tests to check for rotation prop in created nodes --- .../test/api/api_skeleton_latest.spec.ts | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts index e21c41cca07..79cb1027506 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts @@ -5,8 +5,10 @@ import { setTreeGroupsAction } from "viewer/model/actions/skeletontracing_action import { userSettings } from "types/schemas/user_settings.schema"; import Store from "viewer/store"; import { vi, describe, it, expect, beforeEach } from "vitest"; -import type { Vector3 } from "viewer/constants"; +import { OrthoViews, OrthoViewToNumber, type Vector3 } from "viewer/constants"; import { enforceSkeletonTracing } from "viewer/model/accessors/skeletontracing_accessor"; +import { setViewportAction } from "viewer/model/actions/view_mode_actions"; +import { setRotationAction } from "viewer/model/actions/flycam_actions"; // All the mocking is done in the helpers file, so it can be reused for both skeleton and volume API describe("API Skeleton", () => { @@ -299,4 +301,93 @@ describe("API Skeleton", () => { true, ); }); + it("should create skeleton nodes with correct properties", ({ api }) => { + Store.dispatch(setRotationAction([0, 0, 0])); + Store.dispatch(setViewportAction(OrthoViews.PLANE_XY)); + api.tracing.createNode([10, 10, 10]); + const newNode = enforceSkeletonTracing(Store.getState().annotation) + .trees.getOrThrow(2) + .nodes.getOrThrow(4); + const propsToCheck = { + untransformedPosition: newNode.untransformedPosition, + additionalCoordinates: newNode.additionalCoordinates, + rotation: newNode.rotation, + viewport: newNode.viewport, + mag: newNode.mag, + }; + expect(propsToCheck).toStrictEqual({ + untransformedPosition: [10, 10, 10], + additionalCoordinates: [], + rotation: [0, 0, 0], + viewport: OrthoViewToNumber[OrthoViews.PLANE_XY], + mag: 0, + }); + }); + it("should create skeleton nodes with correct properties", ({ api }) => { + Store.dispatch(setRotationAction([0, 0, 0])); + Store.dispatch(setViewportAction(OrthoViews.PLANE_YZ)); + api.tracing.createNode([10, 10, 10]); + const newNode = enforceSkeletonTracing(Store.getState().annotation) + .trees.getOrThrow(2) + .nodes.getOrThrow(4); + const propsToCheck = { + untransformedPosition: newNode.untransformedPosition, + additionalCoordinates: newNode.additionalCoordinates, + rotation: newNode.rotation, + viewport: newNode.viewport, + mag: newNode.mag, + }; + expect(propsToCheck).toStrictEqual({ + untransformedPosition: [10, 10, 10], + additionalCoordinates: [], + rotation: [0, 270, 0], + viewport: OrthoViewToNumber[OrthoViews.PLANE_YZ], + mag: 0, + }); + }); + + it("should create skeleton nodes with correct properties", ({ api }) => { + Store.dispatch(setRotationAction([0, 0, 0])); + Store.dispatch(setViewportAction(OrthoViews.PLANE_XZ)); + api.tracing.createNode([10, 10, 10]); + const newNode = enforceSkeletonTracing(Store.getState().annotation) + .trees.getOrThrow(2) + .nodes.getOrThrow(4); + const propsToCheck = { + untransformedPosition: newNode.untransformedPosition, + additionalCoordinates: newNode.additionalCoordinates, + rotation: newNode.rotation, + viewport: newNode.viewport, + mag: newNode.mag, + }; + expect(propsToCheck).toStrictEqual({ + untransformedPosition: [10, 10, 10], + additionalCoordinates: [], + rotation: [90, 0, 0], + viewport: OrthoViewToNumber[OrthoViews.PLANE_XZ], + mag: 0, + }); + }); + it("should create skeleton nodes with correct rotation when flycam is rotated", ({ + api, + }) => { + Store.dispatch(setRotationAction([20, 90, 10])); + Store.dispatch(setViewportAction(OrthoViews.PLANE_XY)); + api.tracing.createNode([10, 10, 10]); + const newNode = enforceSkeletonTracing(Store.getState().annotation) + .trees.getOrThrow(2) + .nodes.getOrThrow(4); + expect(newNode.rotation).toStrictEqual([20, 90, 10]); + }); + it("should create skeleton nodes with correct rotation when flycam is rotated", ({ + api, + }) => { + Store.dispatch(setRotationAction([20, 90, 0])); + Store.dispatch(setViewportAction(OrthoViews.PLANE_YZ)); + api.tracing.createNode([10, 10, 10]); + const newNode = enforceSkeletonTracing(Store.getState().annotation) + .trees.getOrThrow(2) + .nodes.getOrThrow(4); + expect(newNode.rotation).toStrictEqual([20, 90, 0]); + }); }); From 16c7b16857238c197a64aa10bdc4e4e988c2c79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:39:00 +0200 Subject: [PATCH 090/128] ensure getFlycamRotationWithAppendedRotation returns angles in range of 0-360 --- .../javascripts/viewer/model/accessors/flycam_accessor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index 0509ffb6847..ebf58b03afa 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -357,9 +357,9 @@ export function getFlycamRotationWithAppendedRotation( totalRotationQuaternion.setFromEuler(flycamRotationEuler).multiply(additionalRotationQuaternion); const rotationEuler = totalRotationEuler.setFromQuaternion(totalRotationQuaternion, "ZYX"); const rotationInDegree = map3(THREE.MathUtils.radToDeg, [ - rotationEuler.x, - rotationEuler.y, - rotationEuler.z, + mod(rotationEuler.x, 2 * Math.PI), + mod(rotationEuler.y, 2 * Math.PI), + mod(rotationEuler.z, 2 * Math.PI), ]); console.log( "flycam rotation", From f335325ec0fdd58361de3ea5cb88129896aad416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:40:17 +0200 Subject: [PATCH 091/128] fix default flycam rotation according to default flycam matrix --- frontend/javascripts/viewer/default_state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/viewer/default_state.ts b/frontend/javascripts/viewer/default_state.ts index 7bfae550bc5..1c820b2255a 100644 --- a/frontend/javascripts/viewer/default_state.ts +++ b/frontend/javascripts/viewer/default_state.ts @@ -198,7 +198,7 @@ const defaultState: WebknossosState = { spaceDirectionOrtho: [1, 1, 1], direction: [0, 0, 0], additionalCoordinates: [], - rotation: [0, 0, 0], + rotation: [0, 0, 180], }, flycamInfoCache: { maximumZoomForAllMags: {}, From 0d815a01ebb86b70dfec707589bc5faea9a3d0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:40:36 +0200 Subject: [PATCH 092/128] fix url manager specs --- frontend/javascripts/test/controller/url_manager.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/test/controller/url_manager.spec.ts b/frontend/javascripts/test/controller/url_manager.spec.ts index 707421cd535..4baf9cd3e15 100644 --- a/frontend/javascripts/test/controller/url_manager.spec.ts +++ b/frontend/javascripts/test/controller/url_manager.spec.ts @@ -193,7 +193,7 @@ describe("UrlManager", () => { additionalCoordinates: [], mode, zoomStep: 1.3, - rotation: [0, 0, 180] as Vector3 as Vector3, + rotation: [0, 0, 180] as Vector3, }; const initialState = update(defaultState, { temporaryConfiguration: { @@ -244,7 +244,8 @@ describe("UrlManager", () => { it("should build default url in csv format", () => { UrlManager.initialize(); const url = UrlManager.buildUrl(); - // There is a default rotation of 180 around z which needs to be accounted for here. + // The default state in the store does not include the rotation of 180 degrees around z axis which is always subtracted from the rotation. + // Thus, the rotation of 180 around z is present. expect(url).toBe("#0,0,0,0,1.3,0,0,180"); }); @@ -255,6 +256,9 @@ describe("UrlManager", () => { currentMatrix: { $set: rotationMatrixWithDefaultRotation, }, + rotation: { + $set: [0, 0, 0], + }, }, }); const hash = `#${UrlManager.buildUrlHashCsv(initialState)}`; From 589dc320e815a729c25cf0716149a1809813673f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:24:42 +0200 Subject: [PATCH 093/128] fix cyclic dependencies in tests --- frontend/javascripts/test/controller/url_manager.spec.ts | 2 +- frontend/javascripts/test/fixtures/flycam_object.ts | 5 +++++ frontend/javascripts/test/fixtures/hybridtracing_object.ts | 5 ----- frontend/javascripts/test/fixtures/volumetracing_object.ts | 2 +- frontend/javascripts/test/reducers/flycam_reducer.spec.ts | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 frontend/javascripts/test/fixtures/flycam_object.ts diff --git a/frontend/javascripts/test/controller/url_manager.spec.ts b/frontend/javascripts/test/controller/url_manager.spec.ts index 4baf9cd3e15..e151eaec1aa 100644 --- a/frontend/javascripts/test/controller/url_manager.spec.ts +++ b/frontend/javascripts/test/controller/url_manager.spec.ts @@ -12,7 +12,7 @@ import defaultState from "viewer/default_state"; import update from "immutability-helper"; import DATASET from "../fixtures/dataset_server_object"; import _ from "lodash"; -import { FlycamMatrixWithDefaultRotation } from "test/fixtures/hybridtracing_object"; +import { FlycamMatrixWithDefaultRotation } from "test/fixtures/flycam_object"; describe("UrlManager", () => { it("should replace tracing in url", () => { diff --git a/frontend/javascripts/test/fixtures/flycam_object.ts b/frontend/javascripts/test/fixtures/flycam_object.ts new file mode 100644 index 00000000000..1310255e0fd --- /dev/null +++ b/frontend/javascripts/test/fixtures/flycam_object.ts @@ -0,0 +1,5 @@ +import { M4x4 } from "libs/mjs"; + +// Apply the default 180 z axis rotation to identity matrix as this is always applied on every flycam per default. +// This can be useful in tests to get a calculated rotation of [0, 0, 0]. Otherwise it would be [0, 0, 180]. +export const FlycamMatrixWithDefaultRotation = M4x4.rotate(Math.PI, [0, 0, 1], M4x4.identity(), []); diff --git a/frontend/javascripts/test/fixtures/hybridtracing_object.ts b/frontend/javascripts/test/fixtures/hybridtracing_object.ts index f6768f73ba9..263e4cc9549 100644 --- a/frontend/javascripts/test/fixtures/hybridtracing_object.ts +++ b/frontend/javascripts/test/fixtures/hybridtracing_object.ts @@ -8,7 +8,6 @@ import EdgeCollection from "viewer/model/edge_collection"; import { MISSING_GROUP_ID } from "viewer/view/right-border-tabs/trees_tab/tree_hierarchy_view_helpers"; import { TreeTypeEnum } from "viewer/constants"; import type { APIColorLayer } from "types/api_types"; -import { M4x4 } from "libs/mjs"; const colorLayer: APIColorLayer = { name: "color", @@ -100,7 +99,3 @@ export const initialState = update(defaultState, { }, }, }); - -// Apply the default 180 z axis rotation to identity matrix as this is always applied on every flycam per default. -// This can be useful in tests to get a calculated rotation of [0, 0, 0]. Otherwise it would be [0, 0, 180]. -export const FlycamMatrixWithDefaultRotation = M4x4.rotate(Math.PI, [0, 0, 1], M4x4.identity(), []); diff --git a/frontend/javascripts/test/fixtures/volumetracing_object.ts b/frontend/javascripts/test/fixtures/volumetracing_object.ts index 4afd5104536..2547df3e397 100644 --- a/frontend/javascripts/test/fixtures/volumetracing_object.ts +++ b/frontend/javascripts/test/fixtures/volumetracing_object.ts @@ -2,7 +2,7 @@ import update from "immutability-helper"; import { AnnotationTool } from "viewer/model/accessors/tool_accessor"; import Constants from "viewer/constants"; import defaultState from "viewer/default_state"; -import { FlycamMatrixWithDefaultRotation } from "./hybridtracing_object"; +import { FlycamMatrixWithDefaultRotation } from "./flycam_object"; const volumeTracing = { type: "volume", diff --git a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts index 367e4aac9c1..0800e608aec 100644 --- a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts @@ -11,6 +11,7 @@ import { } from "viewer/model/accessors/flycam_accessor"; import * as FlycamActions from "viewer/model/actions/flycam_actions"; import FlycamReducer from "viewer/model/reducers/flycam_reducer"; +import { FlycamMatrixWithDefaultRotation } from "test/fixtures/flycam_object"; import { describe, it, expect } from "vitest"; function equalWithEpsilon(a: number[], b: number[], epsilon = 1e-10) { From 7230cc04b829d8a835aa49303e439a74feb4cf92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:32:25 +0200 Subject: [PATCH 094/128] add todo comment & more debugging output --- .../test/api/api_skeleton_latest.spec.ts | 1 + .../combinations/skeleton_handlers.ts | 11 ++++--- .../viewer/model/accessors/flycam_accessor.ts | 14 +++++++++ .../viewer/model/reducers/flycam_reducer.ts | 30 ++++++++++++++++++- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts index 79cb1027506..431c09f5ed6 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts @@ -377,6 +377,7 @@ describe("API Skeleton", () => { const newNode = enforceSkeletonTracing(Store.getState().annotation) .trees.getOrThrow(2) .nodes.getOrThrow(4); + // TODOM: Adjust expectation to allow equal euler angles with a little numeric offset! expect(newNode.rotation).toStrictEqual([20, 90, 10]); }); it("should create skeleton nodes with correct rotation when flycam is rotated", ({ diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index 3cd95801cf0..041f63aec0e 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -1,5 +1,5 @@ import { V3 } from "libs/mjs"; -import { map3, values } from "libs/utils"; +import { map3, mod, values } from "libs/utils"; import _ from "lodash"; import * as THREE from "three"; import type { AdditionalCoordinate } from "types/api_types"; @@ -318,11 +318,10 @@ export function getOptionsForCreateSkeletonNode( const flycamOnlyRotation = new THREE.Euler().setFromQuaternion(rotationWithoutQuaternion, "ZYX"); const flycamOnlyRotationInDegree = map3( Math.round, - map3(THREE.MathUtils.radToDeg, [ - flycamOnlyRotation.x, - flycamOnlyRotation.y, - flycamOnlyRotation.z, - ]), + map3( + (a) => mod(THREE.MathUtils.radToDeg(a), 360), + [flycamOnlyRotation.x, flycamOnlyRotation.y, flycamOnlyRotation.z], + ), ); console.log("calculated the following rotation back of the node", flycamOnlyRotationInDegree); diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index ebf58b03afa..a8e117f20dd 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -353,6 +353,10 @@ export function getFlycamRotationWithAppendedRotation( ): Vector3 { const flycamRotation = map3(THREE.MathUtils.degToRad, flycam.rotation); flycamRotationEuler.set(...flycamRotation, "ZYX"); + const rotFlycamMatrix = new THREE.Matrix4() + .makeRotationFromEuler(flycamRotationEuler) + .multiply(new THREE.Matrix4().makeRotationFromEuler(rotationToAppend)); + const rotation = new THREE.Euler().setFromRotationMatrix(rotFlycamMatrix, "ZYX"); additionalRotationQuaternion.setFromEuler(rotationToAppend); totalRotationQuaternion.setFromEuler(flycamRotationEuler).multiply(additionalRotationQuaternion); const rotationEuler = totalRotationEuler.setFromQuaternion(totalRotationQuaternion, "ZYX"); @@ -361,13 +365,23 @@ export function getFlycamRotationWithAppendedRotation( mod(rotationEuler.y, 2 * Math.PI), mod(rotationEuler.z, 2 * Math.PI), ]); + const flycamRotationForthAndBack = new THREE.Euler().setFromQuaternion( + new THREE.Quaternion().setFromEuler(flycamRotationEuler), + "ZYX", + ); console.log( "flycam rotation", flycamRotation, "viewport rotation", rotationToAppend, + "rotationEuler", + rotationEuler, "resulting rotation", rotationInDegree, + "rotation", + rotation, + "flycamRotationForthAndBack", + flycamRotationForthAndBack, ); return rotationInDegree; } diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 24827774592..d82d1c76434 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -114,7 +114,7 @@ function resetMatrix(matrix: Matrix4x4, voxelSize: Vector3) { const scale = getMatrixScale(voxelSize); // Save position const position = [matrix[12], matrix[13], matrix[14]]; - // Reset rotation + // Reset rotation. The default rotation of 180 degree around z is applied. const newMatrix = rotateOnAxis(M4x4.scale(scale, M4x4.identity(), []), Math.PI, [0, 0, 1]); // Restore position newMatrix[12] = position[0]; @@ -186,6 +186,34 @@ export function setRotationReducer(state: WebknossosState, rotation: Vector3) { matrix = rotateOnAxis(matrix, (-z * Math.PI) / 180, [0, 0, 1]); matrix = rotateOnAxis(matrix, (-y * Math.PI) / 180, [0, 1, 0]); matrix = rotateOnAxis(matrix, (-x * Math.PI) / 180, [1, 0, 0]); + const threeMatrix = new THREE.Matrix4().fromArray(matrix); + const position = new THREE.Vector3(); + const scale = new THREE.Vector3(); + const quat = new THREE.Quaternion(); + threeMatrix.decompose(position, quat, scale); + let euler = new THREE.Euler().setFromQuaternion(quat, "ZYX"); + console.log("matrix after setting rotation", { position, scale, quat, euler, threeMatrix }); + + console.log("--------------------testing......----------------------------"); + const testM = rotateOnAxis( + rotateOnAxis( + rotateOnAxis(M4x4.identity(), THREE.MathUtils.degToRad(10), [0, 0, 1]), + THREE.MathUtils.degToRad(90), + [0, 1, 0], + ), + THREE.MathUtils.degToRad(20), + [1, 0, 0], + ); + console.log("test matrix", testM); + const threeM = new THREE.Matrix4().fromArray(testM); + threeM.decompose(position, quat, scale); + euler = new THREE.Euler().setFromQuaternion(quat, "ZYX"); + console.log("untransposed:", { threeM, position, quat, scale, euler }); + const threeM2 = new THREE.Matrix4().fromArray(testM).transpose(); + threeM2.decompose(position, quat, scale); + euler = new THREE.Euler().setFromQuaternion(quat, "ZYX"); + console.log("transposed:", { threeM2, position, quat, scale, euler }); + console.log("--------------------end.--------------------------------"); return update(state, { flycam: { currentMatrix: { From 1a4c4f83d9676238958839889d9ecb162adac2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:13:26 +0200 Subject: [PATCH 095/128] fix node creation rotation test --- .../test/api/api_skeleton_latest.spec.ts | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts index 431c09f5ed6..fc6483ec10b 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts @@ -5,10 +5,17 @@ import { setTreeGroupsAction } from "viewer/model/actions/skeletontracing_action import { userSettings } from "types/schemas/user_settings.schema"; import Store from "viewer/store"; import { vi, describe, it, expect, beforeEach } from "vitest"; -import { OrthoViews, OrthoViewToNumber, type Vector3 } from "viewer/constants"; +import { OrthoBaseRotations, OrthoViews, OrthoViewToNumber, type Vector3 } from "viewer/constants"; import { enforceSkeletonTracing } from "viewer/model/accessors/skeletontracing_accessor"; import { setViewportAction } from "viewer/model/actions/view_mode_actions"; import { setRotationAction } from "viewer/model/actions/flycam_actions"; +import * as THREE from "three"; + +const toRadian = (arr: Vector3): Vector3 => [ + THREE.MathUtils.degToRad(arr[0]), + THREE.MathUtils.degToRad(arr[1]), + THREE.MathUtils.degToRad(arr[2]), +]; // All the mocking is done in the helpers file, so it can be reused for both skeleton and volume API describe("API Skeleton", () => { @@ -368,27 +375,41 @@ describe("API Skeleton", () => { mag: 0, }); }); - it("should create skeleton nodes with correct rotation when flycam is rotated", ({ + it("should create skeleton nodes with correct rotation when flycam is rotated in XY viewport.", ({ api, }) => { - Store.dispatch(setRotationAction([20, 90, 10])); + const rotation = [20, 90, 10] as Vector3; + const rotationQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...toRadian(rotation), "ZYX"), + ); + Store.dispatch(setRotationAction(rotation)); Store.dispatch(setViewportAction(OrthoViews.PLANE_XY)); api.tracing.createNode([10, 10, 10]); const newNode = enforceSkeletonTracing(Store.getState().annotation) .trees.getOrThrow(2) .nodes.getOrThrow(4); - // TODOM: Adjust expectation to allow equal euler angles with a little numeric offset! - expect(newNode.rotation).toStrictEqual([20, 90, 10]); + const newNodeQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...toRadian(newNode.rotation), "ZYX"), + ); + expect(rotationQuaternion.angleTo(newNodeQuaternion)).toBeLessThan(0.000001); }); - it("should create skeleton nodes with correct rotation when flycam is rotated", ({ + it("should create skeleton nodes with correct rotation when flycam is rotated in YZ viewport.", ({ api, }) => { - Store.dispatch(setRotationAction([20, 90, 0])); + const rotation = [20, 90, 0] as Vector3; + const rotationQuaternion = new THREE.Quaternion() + .setFromEuler(new THREE.Euler(...toRadian(rotation), "ZYX")) + // Apply viewport's default rotation. + .multiply(new THREE.Quaternion().setFromEuler(OrthoBaseRotations[OrthoViews.PLANE_YZ])); + Store.dispatch(setRotationAction(rotation)); Store.dispatch(setViewportAction(OrthoViews.PLANE_YZ)); api.tracing.createNode([10, 10, 10]); const newNode = enforceSkeletonTracing(Store.getState().annotation) .trees.getOrThrow(2) .nodes.getOrThrow(4); - expect(newNode.rotation).toStrictEqual([20, 90, 0]); + const newNodeQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...toRadian(newNode.rotation), "ZYX"), + ); + expect(rotationQuaternion.angleTo(newNodeQuaternion)).toBeLessThan(0.000001); }); }); From 8ed14c476eb8a03e6bf25d3d5e396a6f1285f7a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 27 Jun 2025 18:43:12 +0200 Subject: [PATCH 096/128] fix calculation of node rotation by doing same quirky matrix math operations as the flycam reducer does --- frontend/javascripts/viewer/api/api_latest.ts | 14 +++--- .../combinations/skeleton_handlers.ts | 23 --------- .../viewer/model/accessors/flycam_accessor.ts | 50 +++++-------------- .../viewer/model/helpers/rotation_helpers.ts | 44 ++++++++++++++++ .../viewer/model/reducers/flycam_reducer.ts | 28 ----------- .../model/sagas/skeletontracing_saga.ts | 31 +++++------- 6 files changed, 77 insertions(+), 113 deletions(-) create mode 100644 frontend/javascripts/viewer/model/helpers/rotation_helpers.ts diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index 15e2b2130ff..ed51c6cd178 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -1414,23 +1414,23 @@ class TracingApi { const { viewModeData, flycam } = Store.getState(); const { activeViewport } = viewModeData.plane; const curPosition = getPosition(flycam); - const curRotation = getRotationInRadian(flycam); //Utils.map3(THREE.MathUtils.degToRad, flycam.rotation); + // As the node rotation was calculated in the way the flycam reducer does its matrix rotation + // by using the rotation_helpers.ts there is no need to invert z and we must use XYZ ordered euler angles. + const curRotation = getRotationInRadian(flycam, false); const startQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...curRotation, "ZYX"), + new THREE.Euler(...curRotation, "XYZ"), ); const isNotRotated = V3.equals(curRotation, [0, 0, 0]); const dimensionToSkip = skipCenteringAnimationInThirdDimension && activeViewport !== OrthoViews.TDView && isNotRotated ? dimensions.thirdDimensionForPlane(activeViewport) : null; - if (!Array.isArray(rotation)) { + if (rotation == null) { rotation = curRotation; } else { rotation = Utils.map3(THREE.MathUtils.degToRad, rotation); } - const endQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(...rotation, "ZYX")); - rotation = this.getShortestRotation(curRotation, rotation); - console.log(startQuaternion, endQuaternion, rotation); + const endQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(...rotation, "XYZ")); type Tweener = { positionX: number; @@ -1464,7 +1464,7 @@ class TracingApi { ); const interpolatedEuler = new THREE.Euler().setFromQuaternion( interpolatedQuaternion, - "ZYX", + "XYZ", ); const interpolatedEulerInDegree = Utils.map3(THREE.MathUtils.radToDeg, [ interpolatedEuler.x, diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index 041f63aec0e..678057a079c 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -302,29 +302,6 @@ export function getOptionsForCreateSkeletonNode( initialViewportRotation, ); - // TODOM: delete me - const nodeRotationRadian = map3(THREE.MathUtils.degToRad, rotationInDegree); - const nodeRotationQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...nodeRotationRadian, "ZYX"), - ); - const viewportRotationQuaternion = new THREE.Quaternion().setFromEuler( - OrthoBaseRotations[activeViewport || state.viewModeData.plane.activeViewport], - ); - // Invert the rotation of the viewport to get the rotation configured during node creation. - const inverseViewportRotationQuaternion = viewportRotationQuaternion.invert(); - const rotationWithoutQuaternion = nodeRotationQuaternion.multiply( - inverseViewportRotationQuaternion, - ); - const flycamOnlyRotation = new THREE.Euler().setFromQuaternion(rotationWithoutQuaternion, "ZYX"); - const flycamOnlyRotationInDegree = map3( - Math.round, - map3( - (a) => mod(THREE.MathUtils.radToDeg(a), 360), - [flycamOnlyRotation.x, flycamOnlyRotation.y, flycamOnlyRotation.z], - ), - ); - console.log("calculated the following rotation back of the node", flycamOnlyRotationInDegree); - // Center node if the corresponding setting is true. Only pressing CTRL can override this. const center = state.userConfiguration.centerNewNode && !ctrlIsPressed; diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index a8e117f20dd..129ed50e653 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -42,6 +42,10 @@ import { } from "../helpers/transformation_helpers"; import { getMatrixScale, rotateOnAxis } from "../reducers/flycam_reducer"; import { reuseInstanceOnEquality } from "./accessor_helpers"; +import { + eulerAngleToReducerInternalMatrix, + reducerInternalMatrixToEulerAngle, +} from "../helpers/rotation_helpers"; export const ZOOM_STEP_INTERVAL = 1.1; @@ -339,50 +343,22 @@ function _isRotated(flycam: Flycam): boolean { return !V3.equals(getRotationInRadian(flycam), [0, 0, 0]); } -// Already defined here at toplevel to avoid object recreation with each call. Make sure to not do anything async between read and writes. -const flycamRotationEuler = new THREE.Euler(0, 0, 0); -const additionalRotationQuaternion = new THREE.Quaternion(); -const totalRotationQuaternion = new THREE.Quaternion(); -const totalRotationEuler = new THREE.Euler(); - // Memoizing this function makes no sense as its result will always be used to change the flycam rotation. export function getFlycamRotationWithAppendedRotation( flycam: Flycam, // prependedRotation must be in ZYX order. rotationToAppend: THREE.Euler, ): Vector3 { - const flycamRotation = map3(THREE.MathUtils.degToRad, flycam.rotation); - flycamRotationEuler.set(...flycamRotation, "ZYX"); - const rotFlycamMatrix = new THREE.Matrix4() - .makeRotationFromEuler(flycamRotationEuler) - .multiply(new THREE.Matrix4().makeRotationFromEuler(rotationToAppend)); - const rotation = new THREE.Euler().setFromRotationMatrix(rotFlycamMatrix, "ZYX"); - additionalRotationQuaternion.setFromEuler(rotationToAppend); - totalRotationQuaternion.setFromEuler(flycamRotationEuler).multiply(additionalRotationQuaternion); - const rotationEuler = totalRotationEuler.setFromQuaternion(totalRotationQuaternion, "ZYX"); - const rotationInDegree = map3(THREE.MathUtils.radToDeg, [ - mod(rotationEuler.x, 2 * Math.PI), - mod(rotationEuler.y, 2 * Math.PI), - mod(rotationEuler.z, 2 * Math.PI), - ]); - const flycamRotationForthAndBack = new THREE.Euler().setFromQuaternion( - new THREE.Quaternion().setFromEuler(flycamRotationEuler), - "ZYX", - ); - console.log( - "flycam rotation", - flycamRotation, - "viewport rotation", - rotationToAppend, - "rotationEuler", - rotationEuler, - "resulting rotation", - rotationInDegree, - "rotation", - rotation, - "flycamRotationForthAndBack", - flycamRotationForthAndBack, + const flycamRotation = getRotationInRadian(flycam, false); + + // Perform same operations as the flycam reducer does. First default 180° around z. + let rotFlycamMatrix = eulerAngleToReducerInternalMatrix(flycamRotation); + // Apply viewport default rotation + rotFlycamMatrix = rotFlycamMatrix.multiply( + new THREE.Matrix4().makeRotationFromEuler(rotationToAppend), ); + const rotationInRadian = reducerInternalMatrixToEulerAngle(rotFlycamMatrix); + const rotationInDegree = map3(THREE.MathUtils.radToDeg, rotationInRadian); return rotationInDegree; } diff --git a/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts b/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts new file mode 100644 index 00000000000..fcc4ed2f368 --- /dev/null +++ b/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts @@ -0,0 +1,44 @@ +import { V3 } from "libs/mjs"; +import { mod } from "libs/utils"; +import * as THREE from "three"; +import type { Vector3 } from "viewer/constants"; + +// Pre definitions to avoid redundant object creation. +const matrix = new THREE.Matrix4(); +const euler = new THREE.Euler(); +const invertedEulerMatrix = new THREE.Matrix4(); + +// This function performs the same operations as done in the flycam reducer for the setRotation action. +// When rotation calculation are needed in the flycam matrix space, this function can be used to +// first convert the angle into the quirky way the flycam matrix does rotations. +// After the rotation calculation is done, the companion function reducerInternalMatrixToEulerAngle +// should be used to transform the result back. +export function eulerAngleToReducerInternalMatrix(angleInRadian: Vector3): THREE.Matrix4 { + // Perform same operations as the flycam reducer does. First default 180° around z. + let matrixLikeInReducer = matrix.makeRotationZ(Math.PI); + // Invert angle and interpret as ZYX order + const invertedEuler = euler.set(...V3.scale(angleInRadian, -1), "ZYX"); + // Apply inverted ZYX euler. + matrixLikeInReducer = matrixLikeInReducer.multiply( + invertedEulerMatrix.makeRotationFromEuler(invertedEuler), + ); + return matrixLikeInReducer; +} + +// Pre definitions to avoid redundant object creation. +const rotationFromMatrix = new THREE.Euler(); + +// The companion function of eulerAngleToReducerInternalMatrix converting a rotation back from the flycam reducer space. +// The output is in radian and should be interpreted as if in ZYX order. +// Note: The matrix must be a rotation only matrix +export function reducerInternalMatrixToEulerAngle(matrixInReducerFormat: THREE.Matrix4): Vector3 { + const localRotationFromMatrix = rotationFromMatrix.setFromRotationMatrix( + matrixInReducerFormat.transpose(), + "XYZ", + ); + return [ + mod(localRotationFromMatrix.x, 2 * Math.PI), + mod(localRotationFromMatrix.y, 2 * Math.PI), + mod(localRotationFromMatrix.z - Math.PI, 2 * Math.PI), + ]; +} diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index d82d1c76434..756466d8b4b 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -186,34 +186,6 @@ export function setRotationReducer(state: WebknossosState, rotation: Vector3) { matrix = rotateOnAxis(matrix, (-z * Math.PI) / 180, [0, 0, 1]); matrix = rotateOnAxis(matrix, (-y * Math.PI) / 180, [0, 1, 0]); matrix = rotateOnAxis(matrix, (-x * Math.PI) / 180, [1, 0, 0]); - const threeMatrix = new THREE.Matrix4().fromArray(matrix); - const position = new THREE.Vector3(); - const scale = new THREE.Vector3(); - const quat = new THREE.Quaternion(); - threeMatrix.decompose(position, quat, scale); - let euler = new THREE.Euler().setFromQuaternion(quat, "ZYX"); - console.log("matrix after setting rotation", { position, scale, quat, euler, threeMatrix }); - - console.log("--------------------testing......----------------------------"); - const testM = rotateOnAxis( - rotateOnAxis( - rotateOnAxis(M4x4.identity(), THREE.MathUtils.degToRad(10), [0, 0, 1]), - THREE.MathUtils.degToRad(90), - [0, 1, 0], - ), - THREE.MathUtils.degToRad(20), - [1, 0, 0], - ); - console.log("test matrix", testM); - const threeM = new THREE.Matrix4().fromArray(testM); - threeM.decompose(position, quat, scale); - euler = new THREE.Euler().setFromQuaternion(quat, "ZYX"); - console.log("untransposed:", { threeM, position, quat, scale, euler }); - const threeM2 = new THREE.Matrix4().fromArray(testM).transpose(); - threeM2.decompose(position, quat, scale); - euler = new THREE.Euler().setFromQuaternion(quat, "ZYX"); - console.log("transposed:", { threeM2, position, quat, scale, euler }); - console.log("--------------------end.--------------------------------"); return update(state, { flycam: { currentMatrix: { diff --git a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts index 24d78262cb1..0e16960b0fe 100644 --- a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts @@ -94,31 +94,26 @@ import Store from "viewer/store"; import type { MutableNode, Node, NodeMap, Tree, TreeMap } from "../types/tree_types"; import { ensureWkReady } from "./ready_sagas"; import { takeWithBatchActionSupport } from "./saga_helpers"; - -const map3 = Utils.map3; +import { + eulerAngleToReducerInternalMatrix, + reducerInternalMatrixToEulerAngle, +} from "../helpers/rotation_helpers"; function getNodeRotationWithoutPlaneRotation(activeNode: Readonly): Vector3 { // In orthogonal view mode, we need to subtract the - const nodeRotationRadian = map3(THREE.MathUtils.degToRad, activeNode.rotation); - const nodeRotationQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...nodeRotationRadian, "ZYX"), - ); - const viewportRotationQuaternion = new THREE.Quaternion().setFromEuler( + const nodeRotationRadian = Utils.map3(THREE.MathUtils.degToRad, activeNode.rotation); + const nodeRotationInReducerFormatMatrix = eulerAngleToReducerInternalMatrix(nodeRotationRadian); + const viewportRotationMatrix = new THREE.Matrix4().makeRotationFromEuler( OrthoBaseRotations[NumberToOrthoView[activeNode.viewport]], ); // Invert the rotation of the viewport to get the rotation configured during node creation. - const inverseViewportRotationQuaternion = viewportRotationQuaternion.invert(); - const rotationWithoutQuaternion = nodeRotationQuaternion.multiply( - inverseViewportRotationQuaternion, + const viewportRotationMatrixInverted = viewportRotationMatrix.invert(); + const rotationWithoutViewportRotation = nodeRotationInReducerFormatMatrix.multiply( + viewportRotationMatrixInverted, ); - const flycamOnlyRotation = new THREE.Euler().setFromQuaternion(rotationWithoutQuaternion, "ZYX"); - const flycamOnlyRotationInDegree = map3( - Math.round, - map3(THREE.MathUtils.radToDeg, [ - flycamOnlyRotation.x, - flycamOnlyRotation.y, - flycamOnlyRotation.z, - ]), + const rotationInRadian = reducerInternalMatrixToEulerAngle(rotationWithoutViewportRotation); + const flycamOnlyRotationInDegree = V3.round( + Utils.map3(THREE.MathUtils.radToDeg, rotationInRadian), ); return flycamOnlyRotationInDegree; } From e66461c2afc1f6af50c179feee66e4a1629e2fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:39:33 +0200 Subject: [PATCH 097/128] WIP: Fix 3d rotation buttons --- .../viewer/controller/camera_controller.ts | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/frontend/javascripts/viewer/controller/camera_controller.ts b/frontend/javascripts/viewer/controller/camera_controller.ts index fda8215eff9..905def850f8 100644 --- a/frontend/javascripts/viewer/controller/camera_controller.ts +++ b/frontend/javascripts/viewer/controller/camera_controller.ts @@ -39,25 +39,37 @@ function getQuaternionFromCamera( const up = V3.normalize(_up); const forward = V3.normalize(V3.sub(center, position)); const right = V3.normalize(V3.cross(up, forward)); + const correctedUp = V3.normalize(V3.cross(forward, right)); // Create a basis matrix const rotationMatrix = new THREE.Matrix4(); rotationMatrix.makeBasis( new THREE.Vector3(...right), - new THREE.Vector3(...up), + new THREE.Vector3(...correctedUp), new THREE.Vector3(...forward), ); + const alternativeMatrix = new THREE.Matrix4(); + // biome-ignore format: don't format + alternativeMatrix.set(right[0], up[0], forward[0], 0, right[1], up[1], forward[1], 0, right[2], up[2], forward[2], 0, 0, 0, 0, 1); + console.log(rotationMatrix, alternativeMatrix); + // If there's an additional rotation, apply it to the basis matrix if (rotation) { + console.log("applying rotation", rotation); const additionalRotation = new THREE.Matrix4(); additionalRotation.makeRotationFromEuler(new THREE.Euler(...rotation, "ZYX")); // You mentioned ZYX rotationMatrix.premultiply(additionalRotation); // Apply flycamRotation before } // Convert to quaternion + const translation = new THREE.Vector3(); + const scale = new THREE.Vector3(); + const quat2 = new THREE.Quaternion(); const quat = new THREE.Quaternion(); + rotationMatrix.decompose(translation, quat2, scale); quat.setFromRotationMatrix(rotationMatrix); + console.log(quat, quat2); return quat; } @@ -298,29 +310,34 @@ export function rotate3DViewTo( if (id === OrthoViews.TDView && (height <= 0 || width <= 0)) { // This should only be the case when initializing the 3D-viewport. const aspectRatio = getInputCatcherAspectRatio(state, OrthoViews.TDView); - const datasetCenter = voxelToUnit(dataset.dataSource.scale, getDatasetCenter(dataset)); // The camera has no width and height which might be due to a bug or the camera has not been initialized. // Thus we zoom out to show the whole dataset. const paddingFactor = 1.1; width = Math.sqrt(datasetExtent.width ** 2 + datasetExtent.height ** 2) * paddingFactor; height = width / aspectRatio; - up = [0, 0, -1]; + } + if (id === OrthoViews.TDView) { + const positionOffsetVector = new THREE.Vector3( + clippingOffsetFactor, + clippingOffsetFactor, + -clippingOffsetFactor / 2, + ); + const upVector = new THREE.Vector3(0, 0, -1); + // Rotate the positionOffsetVector and upVector by the flycam rotation. + const rotatedOffset = positionOffsetVector.applyEuler( + new THREE.Euler(...flycamRotation, "ZYX"), + ); + const rotatedUp = upVector.applyEuler(new THREE.Euler(...flycamRotation, "ZYX")); // For very tall datasets that have a very low or high z starting coordinate, the planes might not be visible. // Thus take the z coordinate of the flycam instead of the z coordinate of the center. // The clippingOffsetFactor is added in x and y direction to get a view on the dataset the 3D view that is close to the plane views. // Thus the rotation between the 3D view to the eg. XY plane views is much shorter and the interpolated rotation does not look weird. position = [ - datasetCenter[0] + clippingOffsetFactor, - datasetCenter[1] + clippingOffsetFactor, - flycamPos[2] - clippingOffsetFactor, + flycamPos[0] + rotatedOffset.x, + flycamPos[1] + rotatedOffset.y, + flycamPos[2] + rotatedOffset.z, ]; - } else if (id === OrthoViews.TDView) { - position = [ - flycamPos[0] + clippingOffsetFactor, - flycamPos[1] + clippingOffsetFactor, - flycamPos[2] - clippingOffsetFactor, - ]; - up = [0, 0, -1]; + up = [rotatedUp.x, rotatedUp.y, rotatedUp.z]; } else { const positionOffset: OrthoViewMap = { [OrthoViews.PLANE_XY]: [0, 0, -clippingOffsetFactor], @@ -351,7 +368,13 @@ export function rotate3DViewTo( // (radius) to currentFlycamPos constant. Consequently, the camera moves on the // surfaces of a sphere with the center at currentFlycamPos. const startQuaternion = getQuaternionFromCamera(tdCamera.up, tdCamera.position, currentFlycamPos); - const targetQuaternion = getQuaternionFromCamera(up, position, currentFlycamPos, flycamRotation); + if (id === OrthoViews.TDView) { + console.log("calculating td camera from", up, position, flycamPos); + } + const targetQuaternion = + id === OrthoViews.TDView + ? getQuaternionFromCamera(up, position, flycamPos) + : getQuaternionFromCamera(up, position, currentFlycamPos, flycamRotation); const centerDistance = V3.length(V3.sub(currentFlycamPos, position)); const to: TweenState = { left: -width / 2, From b02468c7e76c28e7a3d9f55dd2eae3b8e779eca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:05:13 +0200 Subject: [PATCH 098/128] refactor 3d rotation code --- .../viewer/controller/camera_controller.ts | 115 ++++++------------ 1 file changed, 34 insertions(+), 81 deletions(-) diff --git a/frontend/javascripts/viewer/controller/camera_controller.ts b/frontend/javascripts/viewer/controller/camera_controller.ts index 905def850f8..695419ddefd 100644 --- a/frontend/javascripts/viewer/controller/camera_controller.ts +++ b/frontend/javascripts/viewer/controller/camera_controller.ts @@ -10,7 +10,7 @@ import { OrthoViewValuesWithoutTDView, OrthoViews, } from "viewer/constants"; -import { getDatasetCenter, getDatasetExtentInUnit } from "viewer/model/accessors/dataset_accessor"; +import { getDatasetExtentInUnit } from "viewer/model/accessors/dataset_accessor"; import { getPosition, getRotationInRadian } from "viewer/model/accessors/flycam_accessor"; import { getInputCatcherAspectRatio, @@ -30,15 +30,13 @@ type Props = { setTargetAndFixPosition: () => void; }; -function getQuaternionFromCamera( - _up: Vector3, - position: Vector3, - center: Vector3, - rotation?: Vector3, // flycamRotation in radians -) { +function getQuaternionFromCamera(_up: Vector3, position: Vector3, center: Vector3) { const up = V3.normalize(_up); const forward = V3.normalize(V3.sub(center, position)); const right = V3.normalize(V3.cross(up, forward)); + // Up might not be completely orthogonal to forward, so we need to correct it. + // Such tiny error would else lead to a non-orthogonal basis matrix leading to + // potentially very large errors in the calculated quaternion. const correctedUp = V3.normalize(V3.cross(forward, right)); // Create a basis matrix @@ -49,27 +47,9 @@ function getQuaternionFromCamera( new THREE.Vector3(...forward), ); - const alternativeMatrix = new THREE.Matrix4(); - // biome-ignore format: don't format - alternativeMatrix.set(right[0], up[0], forward[0], 0, right[1], up[1], forward[1], 0, right[2], up[2], forward[2], 0, 0, 0, 0, 1); - console.log(rotationMatrix, alternativeMatrix); - - // If there's an additional rotation, apply it to the basis matrix - if (rotation) { - console.log("applying rotation", rotation); - const additionalRotation = new THREE.Matrix4(); - additionalRotation.makeRotationFromEuler(new THREE.Euler(...rotation, "ZYX")); // You mentioned ZYX - rotationMatrix.premultiply(additionalRotation); // Apply flycamRotation before - } - // Convert to quaternion - const translation = new THREE.Vector3(); - const scale = new THREE.Vector3(); - const quat2 = new THREE.Quaternion(); const quat = new THREE.Quaternion(); - rotationMatrix.decompose(translation, quat2, scale); quat.setFromRotationMatrix(rotationMatrix); - console.log(quat, quat2); return quat; } @@ -300,8 +280,6 @@ export function rotate3DViewTo( // Use width and height to keep the same zoom. let width = tdCamera.right - tdCamera.left; let height = tdCamera.top - tdCamera.bottom; - let position: Vector3; - let up: Vector3; // Way to calculate the position and rotation of the camera: // First, the camera is either positioned at the current center of the flycam or in the dataset center. @@ -316,66 +294,43 @@ export function rotate3DViewTo( width = Math.sqrt(datasetExtent.width ** 2 + datasetExtent.height ** 2) * paddingFactor; height = width / aspectRatio; } - if (id === OrthoViews.TDView) { - const positionOffsetVector = new THREE.Vector3( - clippingOffsetFactor, - clippingOffsetFactor, - -clippingOffsetFactor / 2, - ); - const upVector = new THREE.Vector3(0, 0, -1); - // Rotate the positionOffsetVector and upVector by the flycam rotation. - const rotatedOffset = positionOffsetVector.applyEuler( - new THREE.Euler(...flycamRotation, "ZYX"), - ); - const rotatedUp = upVector.applyEuler(new THREE.Euler(...flycamRotation, "ZYX")); + const positionOffsetMap: OrthoViewMap = { + [OrthoViews.PLANE_XY]: [0, 0, -clippingOffsetFactor], + [OrthoViews.PLANE_YZ]: [clippingOffsetFactor, 0, 0], + [OrthoViews.PLANE_XZ]: [0, clippingOffsetFactor, 0], // For very tall datasets that have a very low or high z starting coordinate, the planes might not be visible. // Thus take the z coordinate of the flycam instead of the z coordinate of the center. // The clippingOffsetFactor is added in x and y direction to get a view on the dataset the 3D view that is close to the plane views. // Thus the rotation between the 3D view to the eg. XY plane views is much shorter and the interpolated rotation does not look weird. - position = [ - flycamPos[0] + rotatedOffset.x, - flycamPos[1] + rotatedOffset.y, - flycamPos[2] + rotatedOffset.z, - ]; - up = [rotatedUp.x, rotatedUp.y, rotatedUp.z]; - } else { - const positionOffset: OrthoViewMap = { - [OrthoViews.PLANE_XY]: [0, 0, -clippingOffsetFactor], - [OrthoViews.PLANE_YZ]: [clippingOffsetFactor, 0, 0], - [OrthoViews.PLANE_XZ]: [0, clippingOffsetFactor, 0], - [OrthoViews.TDView]: [0, 0, 0], - }; - const upVector: OrthoViewMap = { - [OrthoViews.PLANE_XY]: [0, -1, 0], - [OrthoViews.PLANE_YZ]: [0, -1, 0], - [OrthoViews.PLANE_XZ]: [0, 0, -1], - [OrthoViews.TDView]: [0, 0, 0], - }; - up = upVector[id]; - position = [ - positionOffset[id][0] + flycamPos[0], - positionOffset[id][1] + flycamPos[1], - positionOffset[id][2] + flycamPos[2], - ]; - } + // The z offset is halved to have a lower viewing angle at the plane. + [OrthoViews.TDView]: [clippingOffsetFactor, clippingOffsetFactor, -clippingOffsetFactor / 2], + }; + const upVectorMap: OrthoViewMap = { + [OrthoViews.PLANE_XY]: [0, -1, 0], + [OrthoViews.PLANE_YZ]: [0, -1, 0], + [OrthoViews.PLANE_XZ]: [0, 0, -1], + [OrthoViews.TDView]: [0, 0, -1], + }; + + const positionOffsetVector = new THREE.Vector3(...positionOffsetMap[id]); + const upVector = new THREE.Vector3(...upVectorMap[id]); + // Rotate the positionOffsetVector and upVector by the flycam rotation. + const rotatedOffset = positionOffsetVector.applyEuler(new THREE.Euler(...flycamRotation, "ZYX")); + const rotatedUp = upVector.applyEuler(new THREE.Euler(...flycamRotation, "ZYX")); + const position = [ + flycamPos[0] + rotatedOffset.x, + flycamPos[1] + rotatedOffset.y, + flycamPos[2] + rotatedOffset.z, + ] as Vector3; + const up = [rotatedUp.x, rotatedUp.y, rotatedUp.z] as Vector3; - const currentFlycamPos = voxelToUnit( - Store.getState().dataset.dataSource.scale, - getPosition(Store.getState().flycam), - ) || [0, 0, 0]; // Compute current and target orientation as quaternion. When tweening between // these orientations, we compute the new camera position by keeping the distance // (radius) to currentFlycamPos constant. Consequently, the camera moves on the // surfaces of a sphere with the center at currentFlycamPos. - const startQuaternion = getQuaternionFromCamera(tdCamera.up, tdCamera.position, currentFlycamPos); - if (id === OrthoViews.TDView) { - console.log("calculating td camera from", up, position, flycamPos); - } - const targetQuaternion = - id === OrthoViews.TDView - ? getQuaternionFromCamera(up, position, flycamPos) - : getQuaternionFromCamera(up, position, currentFlycamPos, flycamRotation); - const centerDistance = V3.length(V3.sub(currentFlycamPos, position)); + const startQuaternion = getQuaternionFromCamera(tdCamera.up, tdCamera.position, flycamPos); + const targetQuaternion = getQuaternionFromCamera(up, position, flycamPos); + const centerDistance = V3.length(V3.sub(flycamPos, position)); const to: TweenState = { left: -width / 2, right: width / 2, @@ -390,9 +345,7 @@ export function rotate3DViewTo( const tweened = getCameraFromQuaternion(tweenedQuat); // Use forward vector and currentFlycamPos (lookAt target) to calculate the current // camera's position which should be on a sphere (center=currentFlycamPos, radius=centerDistance). - const newPosition = V3.toArray( - V3.sub(currentFlycamPos, V3.scale(tweened.forward, centerDistance)), - ); + const newPosition = V3.toArray(V3.sub(flycamPos, V3.scale(tweened.forward, centerDistance))); Store.dispatch( setTDCameraWithoutTimeTrackingAction({ position: newPosition, From 37fcce3d2550804618c74a6c35153d0e6be35173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:00:19 +0200 Subject: [PATCH 099/128] WIP fix frontend tests --- .../test/api/api_skeleton_latest.spec.ts | 21 ++++++++++++------- .../combinations/skeleton_handlers.ts | 2 +- .../viewer/model/accessors/flycam_accessor.ts | 7 +++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts index fc6483ec10b..b94c0f4e01d 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts @@ -308,7 +308,9 @@ describe("API Skeleton", () => { true, ); }); - it("should create skeleton nodes with correct properties", ({ api }) => { + it("should create skeleton nodes with correct properties for XY viewport", ({ + api, + }) => { Store.dispatch(setRotationAction([0, 0, 0])); Store.dispatch(setViewportAction(OrthoViews.PLANE_XY)); api.tracing.createNode([10, 10, 10]); @@ -330,7 +332,9 @@ describe("API Skeleton", () => { mag: 0, }); }); - it("should create skeleton nodes with correct properties", ({ api }) => { + it("should create skeleton nodes with correct properties for YZ viewport", ({ + api, + }) => { Store.dispatch(setRotationAction([0, 0, 0])); Store.dispatch(setViewportAction(OrthoViews.PLANE_YZ)); api.tracing.createNode([10, 10, 10]); @@ -353,7 +357,9 @@ describe("API Skeleton", () => { }); }); - it("should create skeleton nodes with correct properties", ({ api }) => { + it("should create skeleton nodes with correct properties for XZ viewport", ({ + api, + }) => { Store.dispatch(setRotationAction([0, 0, 0])); Store.dispatch(setViewportAction(OrthoViews.PLANE_XZ)); api.tracing.createNode([10, 10, 10]); @@ -375,12 +381,13 @@ describe("API Skeleton", () => { mag: 0, }); }); + // TODOM: Try figure out why these tests need to compare the quaternions differently. it("should create skeleton nodes with correct rotation when flycam is rotated in XY viewport.", ({ api, }) => { const rotation = [20, 90, 10] as Vector3; const rotationQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...toRadian(rotation), "ZYX"), + new THREE.Euler(...toRadian(rotation)), ); Store.dispatch(setRotationAction(rotation)); Store.dispatch(setViewportAction(OrthoViews.PLANE_XY)); @@ -389,7 +396,7 @@ describe("API Skeleton", () => { .trees.getOrThrow(2) .nodes.getOrThrow(4); const newNodeQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...toRadian(newNode.rotation), "ZYX"), + new THREE.Euler(...toRadian(newNode.rotation)), ); expect(rotationQuaternion.angleTo(newNodeQuaternion)).toBeLessThan(0.000001); }); @@ -398,7 +405,7 @@ describe("API Skeleton", () => { }) => { const rotation = [20, 90, 0] as Vector3; const rotationQuaternion = new THREE.Quaternion() - .setFromEuler(new THREE.Euler(...toRadian(rotation), "ZYX")) + .setFromEuler(new THREE.Euler(...toRadian([-rotation[0], -rotation[1], -rotation[2]]), "ZYX")) // Apply viewport's default rotation. .multiply(new THREE.Quaternion().setFromEuler(OrthoBaseRotations[OrthoViews.PLANE_YZ])); Store.dispatch(setRotationAction(rotation)); @@ -408,7 +415,7 @@ describe("API Skeleton", () => { .trees.getOrThrow(2) .nodes.getOrThrow(4); const newNodeQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...toRadian(newNode.rotation), "ZYX"), + new THREE.Euler(...toRadian(newNode.rotation)), ); expect(rotationQuaternion.angleTo(newNodeQuaternion)).toBeLessThan(0.000001); }); diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index 678057a079c..6df28f7764a 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -1,5 +1,5 @@ import { V3 } from "libs/mjs"; -import { map3, mod, values } from "libs/utils"; +import { values } from "libs/utils"; import _ from "lodash"; import * as THREE from "three"; import type { AdditionalCoordinate } from "types/api_types"; diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index 129ed50e653..beefad8fdbb 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -350,6 +350,13 @@ export function getFlycamRotationWithAppendedRotation( rotationToAppend: THREE.Euler, ): Vector3 { const flycamRotation = getRotationInRadian(flycam, false); + if (V3.equals(flycamRotation, [0, 0, 0])) { + return map3(THREE.MathUtils.radToDeg, [ + rotationToAppend.x, + rotationToAppend.y, + rotationToAppend.z, + ]); + } // Perform same operations as the flycam reducer does. First default 180° around z. let rotFlycamMatrix = eulerAngleToReducerInternalMatrix(flycamRotation); From c59b7a92c1b4dfaaeed7f78e483313c583e77649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:00:48 +0200 Subject: [PATCH 100/128] import sorting --- .../javascripts/viewer/model/accessors/flycam_accessor.ts | 8 ++++---- .../viewer/model/sagas/skeletontracing_saga.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index beefad8fdbb..313a545a48d 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -34,6 +34,10 @@ import * as scaleInfo from "viewer/model/scaleinfo"; import { getBaseVoxelInUnit } from "viewer/model/scaleinfo"; import type { DataLayerType, Flycam, LoadingStrategy, WebknossosState } from "viewer/store"; import type { SmallerOrHigherInfo } from "../helpers/mag_info"; +import { + eulerAngleToReducerInternalMatrix, + reducerInternalMatrixToEulerAngle, +} from "../helpers/rotation_helpers"; import { type Transform, chainTransforms, @@ -42,10 +46,6 @@ import { } from "../helpers/transformation_helpers"; import { getMatrixScale, rotateOnAxis } from "../reducers/flycam_reducer"; import { reuseInstanceOnEquality } from "./accessor_helpers"; -import { - eulerAngleToReducerInternalMatrix, - reducerInternalMatrixToEulerAngle, -} from "../helpers/rotation_helpers"; export const ZOOM_STEP_INTERVAL = 1.1; diff --git a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts index 0e16960b0fe..e8fccd64593 100644 --- a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts @@ -91,13 +91,13 @@ import { api } from "viewer/singletons"; import type { UserBoundingBox } from "viewer/store"; import type { Flycam, SkeletonTracing, WebknossosState } from "viewer/store"; import Store from "viewer/store"; -import type { MutableNode, Node, NodeMap, Tree, TreeMap } from "../types/tree_types"; -import { ensureWkReady } from "./ready_sagas"; -import { takeWithBatchActionSupport } from "./saga_helpers"; import { eulerAngleToReducerInternalMatrix, reducerInternalMatrixToEulerAngle, } from "../helpers/rotation_helpers"; +import type { MutableNode, Node, NodeMap, Tree, TreeMap } from "../types/tree_types"; +import { ensureWkReady } from "./ready_sagas"; +import { takeWithBatchActionSupport } from "./saga_helpers"; function getNodeRotationWithoutPlaneRotation(activeNode: Readonly): Vector3 { // In orthogonal view mode, we need to subtract the From 75b6146e26e989c7449ea20ba417ea0940a3a62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:34:07 +0200 Subject: [PATCH 101/128] add missing import --- frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts index ee1540db47f..b1dd72d062f 100644 --- a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts @@ -2,6 +2,7 @@ import { getAgglomerateSkeleton, getEditableAgglomerateSkeleton } from "admin/re import { Modal } from "antd"; import DiffableMap, { diffDiffableMaps } from "libs/diffable_map"; import ErrorHandling from "libs/error_handling"; +import { V3 } from "libs/mjs"; import createProgressCallback from "libs/progress_callback"; import type { Message } from "libs/toast"; import Toast from "libs/toast"; From a062533deefad9b9d542459acb72765dfecf2730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:45:05 +0200 Subject: [PATCH 102/128] =?UTF-8?q?do=20not=20loop=2090=C2=B0=20rotation?= =?UTF-8?q?=20keyboard=20shortcuts=20and=20fix=20this=20rotation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/viewmodes/plane_controller.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx b/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx index 00b13b3986c..6d7517b101f 100644 --- a/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx +++ b/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx @@ -396,9 +396,9 @@ class PlaneController extends React.PureComponent { ) => { const state = Store.getState(); const invertingFactor = oppositeDirection ? -1 : 1; - const rotationAngle = fixedStepRotation - ? Math.PI / 2 - : state.userConfiguration.rotateValue * timeFactor * invertingFactor; + const rotationAngle = + (fixedStepRotation ? Math.PI / 2 : state.userConfiguration.rotateValue * timeFactor) * + invertingFactor; const { activeViewport } = state.viewModeData.plane; const viewportIndices = Dimensions.getIndices(activeViewport); const rotationAction = axisIndexToRotation[viewportIndices[dimensionIndex]]; @@ -413,7 +413,6 @@ class PlaneController extends React.PureComponent { event.preventDefault(); } }); - const ignoredTimeFactor = 0; this.input.keyboard = new InputKeyboard({ // Move left: (timeFactor) => MoveHandlers.moveU(-getMoveOffset(Store.getState(), timeFactor)), @@ -421,19 +420,11 @@ class PlaneController extends React.PureComponent { up: (timeFactor) => MoveHandlers.moveV(-getMoveOffset(Store.getState(), timeFactor)), down: (timeFactor) => MoveHandlers.moveV(getMoveOffset(Store.getState(), timeFactor)), "shift + left": (timeFactor: number) => rotateViewportAware(timeFactor, 1, false), - // Directly rotate by 90 degrees. - // TODOM: seems to be buggy when switching between shift + ctrl + left and right. Dont know why. - "ctrl + shift + left": () => rotateViewportAware(ignoredTimeFactor, 1, false, true), "shift + right": (timeFactor: number) => rotateViewportAware(timeFactor, 1, true), - "ctrl + shift + right": () => rotateViewportAware(ignoredTimeFactor, 1, true, true), "shift + up": (timeFactor: number) => rotateViewportAware(timeFactor, 0, false), - "ctrl + shift + up": () => rotateViewportAware(ignoredTimeFactor, 0, false, true), "shift + down": (timeFactor: number) => rotateViewportAware(timeFactor, 0, true), - "ctrl + shift + down": () => rotateViewportAware(ignoredTimeFactor, 0, true, true), "alt + left": (timeFactor: number) => rotateViewportAware(timeFactor, 2, false), - "ctrl + alt + left": () => rotateViewportAware(ignoredTimeFactor, 2, false, true), "alt + right": (timeFactor: number) => rotateViewportAware(timeFactor, 2, true), - "ctrl + alt + right": () => rotateViewportAware(ignoredTimeFactor, 0, true, true), }); const { baseControls: notLoopedKeyboardControls, @@ -467,8 +458,18 @@ class PlaneController extends React.PureComponent { delay: Store.getState().userConfiguration.keyboardDelay, }, ); + const ignoredTimeFactor = 0; this.input.keyboardNoLoop = new InputKeyboardNoLoop( - notLoopedKeyboardControls, + { + ...notLoopedKeyboardControls, + // Directly rotate by 90 degrees. + "ctrl + shift + left": () => rotateViewportAware(ignoredTimeFactor, 1, false, true), + "ctrl + shift + right": () => rotateViewportAware(ignoredTimeFactor, 1, true, true), + "ctrl + shift + up": () => rotateViewportAware(ignoredTimeFactor, 0, false, true), + "ctrl + shift + down": () => rotateViewportAware(ignoredTimeFactor, 0, true, true), + "ctrl + alt + left": () => rotateViewportAware(ignoredTimeFactor, 2, false, true), + "ctrl + alt + right": () => rotateViewportAware(ignoredTimeFactor, 0, true, true), + }, {}, extendedNotLoopedKeyboardControls, keyUpControls, From 3d11c6db56dbaa1f99c6ae1135f4dbd05fd93c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:45:13 +0200 Subject: [PATCH 103/128] fix typo --- frontend/javascripts/viewer/api/api_latest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index 5d7af9fcf31..38c315d04db 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -361,7 +361,7 @@ class TracingApi { * coordinates [x, y, z] helps maintain a consistent viewing slice. This prevents the viewports from jumping * between slices due to the animation. * - * In scenarios without raseotation or centering animation, rounded integer coordinates are sufficient. + * In scenarios without rotation or centering animation, rounded integer coordinates are sufficient. */ createNode( position: Vector3, From 8c6c92982360debeafa22906b3067ad7b3bcfe74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 1 Jul 2025 18:30:02 +0200 Subject: [PATCH 104/128] small clean up here and there --- .../viewer/controller/camera_controller.ts | 22 +++++-------------- .../materials/plane_material_factory.ts | 11 +--------- .../viewer/model/accessors/flycam_accessor.ts | 1 - .../viewer/model/helpers/rotation_helpers.ts | 4 ++-- .../viewer/model/reducers/flycam_reducer.ts | 3 --- .../viewer/shaders/main_data_shaders.glsl.ts | 1 - 6 files changed, 9 insertions(+), 33 deletions(-) diff --git a/frontend/javascripts/viewer/controller/camera_controller.ts b/frontend/javascripts/viewer/controller/camera_controller.ts index 695419ddefd..55c0aa5dcb0 100644 --- a/frontend/javascripts/viewer/controller/camera_controller.ts +++ b/frontend/javascripts/viewer/controller/camera_controller.ts @@ -176,26 +176,16 @@ class CameraController extends React.PureComponent { const globalPosition = getPosition(state.flycam); // camera position's unit is nm, so convert it. const cameraPosition = voxelToUnit(state.dataset.dataSource.scale, globalPosition); - this.props.cameras[OrthoViews.PLANE_XY].position.set( - cameraPosition[0], - cameraPosition[1], - cameraPosition[2], - ); - this.props.cameras[OrthoViews.PLANE_YZ].position.set( - cameraPosition[0], - cameraPosition[1], - cameraPosition[2], - ); - this.props.cameras[OrthoViews.PLANE_XZ].position.set( - cameraPosition[0], - cameraPosition[1], - cameraPosition[2], - ); // Now set rotation for all cameras respecting the base rotation of each camera. const globalRotation = getRotationInRadian(state.flycam); this.flycamRotationEuler.set(globalRotation[0], globalRotation[1], globalRotation[2], "ZYX"); this.flycamRotationMatrix.makeRotationFromEuler(this.flycamRotationEuler); for (const viewport of OrthoViewValuesWithoutTDView) { + this.props.cameras[viewport].position.set( + cameraPosition[0], + cameraPosition[1], + cameraPosition[2], + ); this.baseRotationMatrix.makeRotationFromEuler(OrthoCamerasBaseRotations[viewport]); this.props.cameras[viewport].setRotationFromMatrix( this.totalRotationMatrix @@ -238,7 +228,7 @@ class CameraController extends React.PureComponent { // TD-View methods updateTDCamera(cameraData: CameraData): void { const tdCamera = this.props.cameras[OrthoViews.TDView]; - tdCamera.position.set(...(cameraData.position as Vector3)); + tdCamera.position.set(...cameraData.position); tdCamera.left = cameraData.left; tdCamera.right = cameraData.right; tdCamera.top = cameraData.top; diff --git a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts index c035141b10d..cd50447fdf4 100644 --- a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts +++ b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts @@ -597,7 +597,6 @@ class PlaneMaterialFactory { }, ), listenToStoreProperty( - // TODOM: Fix rotating pattern. Rotation around z is also not fixed // TODOM: Fix rotation direction. Seems to be in different direction than actual rotation. (storeState) => getRotationInRadian(storeState.flycam), (rotation) => { @@ -607,15 +606,7 @@ class PlaneMaterialFactory { const toOrigin = new THREE.Matrix4().makeTranslation(...Utils.map3((p) => -p, position)); const backToFlycamCenter = new THREE.Matrix4().makeTranslation(...position); const invertRotation = new THREE.Matrix4() - .makeRotationFromEuler( - // TODOM: Investigate why the + Math.PI solves the Streching error - new THREE.Euler( - rotation[0], // + Math.PI, - rotation[1], // + Math.PI, - rotation[2], // + Math.PI, - "ZYX", - ), - ) + .makeRotationFromEuler(new THREE.Euler(rotation[0], rotation[1], rotation[2], "ZYX")) .invert(); const inverseFlycamRotationMatrix = toOrigin .multiply(invertRotation) diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index 313a545a48d..30c0588b804 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -557,7 +557,6 @@ export function getPlaneExtentInVoxel( return [width * zoomStep, height * zoomStep]; } -// TODOM: Investigate why these values are different to OrthoBaseRotations. export type Area = { left: number; top: number; diff --git a/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts b/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts index fcc4ed2f368..519c395ca67 100644 --- a/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts +++ b/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts @@ -30,10 +30,10 @@ const rotationFromMatrix = new THREE.Euler(); // The companion function of eulerAngleToReducerInternalMatrix converting a rotation back from the flycam reducer space. // The output is in radian and should be interpreted as if in ZYX order. -// Note: The matrix must be a rotation only matrix +// Note: The matrix must be a rotation only matrix. export function reducerInternalMatrixToEulerAngle(matrixInReducerFormat: THREE.Matrix4): Vector3 { const localRotationFromMatrix = rotationFromMatrix.setFromRotationMatrix( - matrixInReducerFormat.transpose(), + matrixInReducerFormat.clone().transpose(), "XYZ", ); return [ diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 756466d8b4b..c377d4fae38 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -63,9 +63,6 @@ function rotateOnAxisWithDistance( function keepRotationInBounds(rotation: Vector3): Vector3 { const rotationInBounds = Utils.map3((v) => Utils.mod(Math.round(v), 360), rotation); - if (rotationInBounds[1] === 90) { - rotationInBounds[1] += 10 ** -5; - } return rotationInBounds; } diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index c4cb183d6ed..eea022a8a35 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -524,7 +524,6 @@ void main() { vec3 voxelSizeFactorInvertedUVW = transDim(voxelSizeFactorInverted); vec3 transWorldCoord = transDim(worldCoord.xyz); - // TODOM: ask why this special case calculation is needed anyway. transWorldCoord.x = ( // Left border of left-most bucket (probably outside of visible plane) From b43849e1ef9fe7ac8323d06c89931c7e94d1610e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:16:43 +0200 Subject: [PATCH 105/128] fix fly mode with rotation 0,0,0 --- frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts | 2 +- frontend/javascripts/viewer/shaders/texture_access.glsl.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index eea022a8a35..d91bb3972d2 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -484,7 +484,7 @@ void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); // Early return shader as optimized vertex positioning at bucket borders currently does not work while rotations are active. // This shouldn't really impact the performance as isFlycamRotated is a uniform. - if(isFlycamRotated){ + if(isFlycamRotated || !<%= isOrthogonal %>) { return; } // Remember the original z position, since it can subtly diverge in the diff --git a/frontend/javascripts/viewer/shaders/texture_access.glsl.ts b/frontend/javascripts/viewer/shaders/texture_access.glsl.ts index 3f2085091cd..94f4b4645e1 100644 --- a/frontend/javascripts/viewer/shaders/texture_access.glsl.ts +++ b/frontend/javascripts/viewer/shaders/texture_access.glsl.ts @@ -206,7 +206,7 @@ export const getColorForCoords: ShaderModule = { // To avoid rare rendering artifacts, don't use the precomputed // bucket address when being at the border of buckets. - bool beSafe = isFlycamRotated; + bool beSafe = isFlycamRotated || !<%= isOrthogonal %>; { renderedMagIdx = outputMagIdx[globalLayerIndex]; vec3 coords = floor(getAbsoluteCoords(worldPositionUVW, renderedMagIdx, globalLayerIndex)); From aead902d5d5f4df5c748e4b8aae0cfa575e89452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:24:52 +0200 Subject: [PATCH 106/128] fix rotation applying when activating node --- .../viewer/model/sagas/skeletontracing_saga.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts index b1dd72d062f..0fd594d6d32 100644 --- a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts @@ -83,13 +83,13 @@ import { api } from "viewer/singletons"; import type { SkeletonTracing, WebknossosState } from "viewer/store"; import Store from "viewer/store"; import { diffBoundingBoxes, diffGroups } from "../helpers/diff_helpers"; +import type { MutableNode, Node, NodeMap, Tree, TreeMap } from "../types/tree_types"; +import { ensureWkReady } from "./ready_sagas"; +import { takeWithBatchActionSupport } from "./saga_helpers"; import { eulerAngleToReducerInternalMatrix, reducerInternalMatrixToEulerAngle, } from "../helpers/rotation_helpers"; -import type { MutableNode, Node, NodeMap, Tree, TreeMap } from "../types/tree_types"; -import { ensureWkReady } from "./ready_sagas"; -import { takeWithBatchActionSupport } from "./saga_helpers"; function getNodeRotationWithoutPlaneRotation(activeNode: Readonly): Vector3 { // In orthogonal view mode, we need to subtract the @@ -131,7 +131,9 @@ function* centerActiveNode(action: Action): Saga { (state: WebknossosState) => state.userConfiguration.applyNodeRotationOnActivation, ); const applyRotation = - ("suppressRotation" in action && !action.suppressRotation) ?? userApplyRotation; + "suppressRotation" in action && action.suppressRotation != null + ? !action.suppressRotation + : userApplyRotation; if (activeNode != null) { let nodeRotation = activeNode.rotation; From feef58e4989762d7af9f580a2227b12968051e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:38:37 +0200 Subject: [PATCH 107/128] fix applying node rotation of node created in arbitrary view --- frontend/javascripts/viewer/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/javascripts/viewer/constants.ts b/frontend/javascripts/viewer/constants.ts index cebd49d89ee..6390fae9e56 100644 --- a/frontend/javascripts/viewer/constants.ts +++ b/frontend/javascripts/viewer/constants.ts @@ -113,6 +113,7 @@ export const NumberToOrthoView: Record = { 1: OrthoViews.PLANE_YZ, 2: OrthoViews.PLANE_XZ, 3: OrthoViews.TDView, + 4: OrthoViews.PLANE_XY, // Arbitrary view is equal to the XY plane. }; const PINK = 0xeb4b98; From 86a67883c4c4b1bf850725e9c7ce23ad6be4a8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:36:17 +0200 Subject: [PATCH 108/128] add changelog entry --- unreleased_changes/8614.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 unreleased_changes/8614.md diff --git a/unreleased_changes/8614.md b/unreleased_changes/8614.md new file mode 100644 index 00000000000..e7e29f6e26b --- /dev/null +++ b/unreleased_changes/8614.md @@ -0,0 +1,2 @@ +### Added +- Added the possibility to rotated the planes in ortho view. While rotated, volume annotation is disabled. \ No newline at end of file From ade03c622df6a4d3aa431ee6bfc0973872effb3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:42:23 +0200 Subject: [PATCH 109/128] sort imports --- .../javascripts/viewer/model/sagas/skeletontracing_saga.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts index 0fd594d6d32..2fdf489ad19 100644 --- a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts @@ -83,13 +83,13 @@ import { api } from "viewer/singletons"; import type { SkeletonTracing, WebknossosState } from "viewer/store"; import Store from "viewer/store"; import { diffBoundingBoxes, diffGroups } from "../helpers/diff_helpers"; -import type { MutableNode, Node, NodeMap, Tree, TreeMap } from "../types/tree_types"; -import { ensureWkReady } from "./ready_sagas"; -import { takeWithBatchActionSupport } from "./saga_helpers"; import { eulerAngleToReducerInternalMatrix, reducerInternalMatrixToEulerAngle, } from "../helpers/rotation_helpers"; +import type { MutableNode, Node, NodeMap, Tree, TreeMap } from "../types/tree_types"; +import { ensureWkReady } from "./ready_sagas"; +import { takeWithBatchActionSupport } from "./saga_helpers"; function getNodeRotationWithoutPlaneRotation(activeNode: Readonly): Vector3 { // In orthogonal view mode, we need to subtract the From 25361cf9393bef69c35e1d9a3ee444d975e4b643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:48:23 +0200 Subject: [PATCH 110/128] apply feedback --- frontend/javascripts/viewer/constants.ts | 2 +- .../controller/viewmodes/plane_controller.tsx | 24 +++++++---- .../model/sagas/skeletontracing_saga.ts | 3 +- .../javascripts/viewer/shaders/coords.glsl.ts | 42 +++++++------------ .../left-border-tabs/layer_settings_tab.tsx | 2 +- unreleased_changes/8614.md | 2 +- 6 files changed, 35 insertions(+), 40 deletions(-) diff --git a/frontend/javascripts/viewer/constants.ts b/frontend/javascripts/viewer/constants.ts index 1b0024c2991..af7ff3a451b 100644 --- a/frontend/javascripts/viewer/constants.ts +++ b/frontend/javascripts/viewer/constants.ts @@ -149,7 +149,7 @@ function correctCameraViewingDirection(baseEuler: THREE.Euler): THREE.Euler { } // The orthographic cameras point towards negative z direction per default. To make it look into positive direction of the z axis, -// an additional rotation around x axis by 180° is needed. Th +// an additional rotation around x axis by 180° is needed. This is appended via correctCameraViewingDirection. export const OrthoCamerasBaseRotations = { [OrthoViews.PLANE_XY]: correctCameraViewingDirection(OrthoBaseRotations[OrthoViews.PLANE_XY]), [OrthoViews.PLANE_YZ]: correctCameraViewingDirection(OrthoBaseRotations[OrthoViews.PLANE_YZ]), diff --git a/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx b/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx index 8c8cc77f8aa..6e922405fb5 100644 --- a/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx +++ b/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx @@ -95,6 +95,8 @@ function ensureNonConflictingHandlers( } } +const FIXED_ROTATION_STEP = Math.PI / 2; + const cycleTools = () => { Store.dispatch(cycleToolAction()); }; @@ -397,8 +399,9 @@ class PlaneController extends React.PureComponent { const state = Store.getState(); const invertingFactor = oppositeDirection ? -1 : 1; const rotationAngle = - (fixedStepRotation ? Math.PI / 2 : state.userConfiguration.rotateValue * timeFactor) * - invertingFactor; + (fixedStepRotation + ? FIXED_ROTATION_STEP + : state.userConfiguration.rotateValue * timeFactor) * invertingFactor; const { activeViewport } = state.viewModeData.plane; const viewportIndices = Dimensions.getIndices(activeViewport); const rotationAction = axisIndexToRotation[viewportIndices[dimensionIndex]]; @@ -458,17 +461,22 @@ class PlaneController extends React.PureComponent { delay: Store.getState().userConfiguration.keyboardDelay, }, ); + const ignoredTimeFactor = 0; + const rotateViewportAwareFixedWithoutTiming = ( + dimensionIndex: DimensionIndices, + oppositeDirection: boolean, + ) => rotateViewportAware(ignoredTimeFactor, dimensionIndex, oppositeDirection, true); this.input.keyboardNoLoop = new InputKeyboardNoLoop( { ...notLoopedKeyboardControls, // Directly rotate by 90 degrees. - "ctrl + shift + left": () => rotateViewportAware(ignoredTimeFactor, 1, false, true), - "ctrl + shift + right": () => rotateViewportAware(ignoredTimeFactor, 1, true, true), - "ctrl + shift + up": () => rotateViewportAware(ignoredTimeFactor, 0, false, true), - "ctrl + shift + down": () => rotateViewportAware(ignoredTimeFactor, 0, true, true), - "ctrl + alt + left": () => rotateViewportAware(ignoredTimeFactor, 2, false, true), - "ctrl + alt + right": () => rotateViewportAware(ignoredTimeFactor, 0, true, true), + "ctrl + shift + left": () => rotateViewportAwareFixedWithoutTiming(1, false), + "ctrl + shift + right": () => rotateViewportAwareFixedWithoutTiming(1, true), + "ctrl + shift + up": () => rotateViewportAwareFixedWithoutTiming(0, false), + "ctrl + shift + down": () => rotateViewportAwareFixedWithoutTiming(0, true), + "ctrl + alt + left": () => rotateViewportAwareFixedWithoutTiming(2, false), + "ctrl + alt + right": () => rotateViewportAwareFixedWithoutTiming(0, true), }, {}, extendedNotLoopedKeyboardControls, diff --git a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts index cbf66ef92b9..013b71e0cd0 100644 --- a/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts @@ -92,7 +92,8 @@ import { ensureWkReady } from "./ready_sagas"; import { takeWithBatchActionSupport } from "./saga_helpers"; function getNodeRotationWithoutPlaneRotation(activeNode: Readonly): Vector3 { - // In orthogonal view mode, we need to subtract the + // In orthogonal view mode, this active planes default rotation is added to the flycam rotation upon node creation. + // To get the same flycam rotation as was active during node creation, the default rotation is calculated out from the nodes rotation. const nodeRotationRadian = map3(THREE.MathUtils.degToRad, activeNode.rotation); const nodeRotationInReducerFormatMatrix = eulerAngleToReducerInternalMatrix(nodeRotationRadian); const viewportRotationMatrix = new THREE.Matrix4().makeRotationFromEuler( diff --git a/frontend/javascripts/viewer/shaders/coords.glsl.ts b/frontend/javascripts/viewer/shaders/coords.glsl.ts index b79d54f5412..2b6355cb8c4 100644 --- a/frontend/javascripts/viewer/shaders/coords.glsl.ts +++ b/frontend/javascripts/viewer/shaders/coords.glsl.ts @@ -25,10 +25,11 @@ export const getAbsoluteCoords: ShaderModule = { } `, }; -export const getWorldCoordUVW: ShaderModule = { + +export const worldCoordToUVW: ShaderModule = { requirements: [getW, isFlightMode], code: ` - vec3 getWorldCoordUVW() { + vec3 worldCoordToUVW(vec4 worldCoord) { vec3 worldCoordUVW = transDim(worldCoord.xyz); vec3 positionOffsetUVW = transDim(positionOffset); @@ -57,35 +58,20 @@ export const getWorldCoordUVW: ShaderModule = { `, }; -// TODOM: refactor me +export const getWorldCoordUVW: ShaderModule = { + requirements: [worldCoordToUVW], + code: ` + vec3 getWorldCoordUVW() { + return worldCoordToUVW(worldCoord); + } + `, +}; + export const getUnrotatedWorldCoordUVW: ShaderModule = { - requirements: [getW, isFlightMode], + requirements: [worldCoordToUVW], code: ` vec3 getUnrotatedWorldCoordUVW() { - vec3 worldCoordUVW = transDim((inverseFlycamRotationMatrix * worldCoord).xyz); - vec3 positionOffsetUVW = transDim(positionOffset); - - if (isFlightMode()) { - vec4 modelCoords = inverseMatrix(savedModelMatrix) * worldCoord; - float sphericalRadius = sphericalCapRadius; - - vec4 centerVertex = vec4(0.0, 0.0, -sphericalRadius, 0.0); - modelCoords.z = 0.0; - modelCoords += centerVertex; - modelCoords.xyz = modelCoords.xyz * (sphericalRadius / length(modelCoords.xyz)); - modelCoords -= centerVertex; - - worldCoordUVW = (savedModelMatrix * modelCoords).xyz; - } - - vec3 voxelSizeFactorInvertedUVW = transDim(voxelSizeFactorInverted); - - // We subtract the potential offset of the plane and then - // need to multiply by voxelSizeFactorInvertedUVW because the threejs scene is scaled. - worldCoordUVW = (worldCoordUVW - positionOffsetUVW) * voxelSizeFactorInvertedUVW; - - - return worldCoordUVW; + return worldCoordToUVW(inverseFlycamRotationMatrix * worldCoord); } `, }; diff --git a/frontend/javascripts/viewer/view/left-border-tabs/layer_settings_tab.tsx b/frontend/javascripts/viewer/view/left-border-tabs/layer_settings_tab.tsx index 2a708b74829..3d7f5f44c9c 100644 --- a/frontend/javascripts/viewer/view/left-border-tabs/layer_settings_tab.tsx +++ b/frontend/javascripts/viewer/view/left-border-tabs/layer_settings_tab.tsx @@ -1306,7 +1306,7 @@ class DatasetSettings extends React.PureComponent { label={settings.applyNodeRotationOnActivation} value={userConfiguration.applyNodeRotationOnActivation} onChange={this.onChangeUser.applyNodeRotationOnActivation} - tooltipText="When enabled, the rotation active during creation of the new active node will be set upon activation." + tooltipText="If enabled, the rotation that was active when a node was created will be set when activating the node." /> Date: Tue, 8 Jul 2025 16:22:03 +0200 Subject: [PATCH 111/128] improve and add node creation rotation tests --- .../test/api/api_skeleton_latest.spec.ts | 89 +++++++++++-------- .../test/fixtures/test_rotations.ts | 15 ++++ .../test/misc/rotation_helpers.spec.ts | 66 ++++++++++++++ .../viewer/model/accessors/flycam_accessor.ts | 7 -- 4 files changed, 134 insertions(+), 43 deletions(-) create mode 100644 frontend/javascripts/test/fixtures/test_rotations.ts create mode 100644 frontend/javascripts/test/misc/rotation_helpers.spec.ts diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts index 866fbf8aae0..b40ae580653 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts @@ -5,11 +5,23 @@ import { setTreeGroupsAction } from "viewer/model/actions/skeletontracing_action import { userSettings } from "types/schemas/user_settings.schema"; import Store from "viewer/store"; import { vi, describe, it, expect, beforeEach } from "vitest"; -import { OrthoBaseRotations, OrthoViews, OrthoViewToNumber, type Vector3 } from "viewer/constants"; +import { + OrthoBaseRotations, + OrthoViews, + OrthoViewToNumber, + OrthoViewValuesWithoutTDView, + type Vector3, +} from "viewer/constants"; import { enforceSkeletonTracing } from "viewer/model/accessors/skeletontracing_accessor"; import { setViewportAction } from "viewer/model/actions/view_mode_actions"; import { setRotationAction } from "viewer/model/actions/flycam_actions"; import * as THREE from "three"; +import { + eulerAngleToReducerInternalMatrix, + reducerInternalMatrixToEulerAngle, +} from "viewer/model/helpers/rotation_helpers"; +import testRotations from "test/fixtures/test_rotations"; +import { map3 } from "libs/utils"; const toRadian = (arr: Vector3): Vector3 => [ THREE.MathUtils.degToRad(arr[0]), @@ -380,42 +392,47 @@ describe("API Skeleton", () => { mag: 0, }); }); - // TODOM: Try figure out why these tests need to compare the quaternions differently. - it("should create skeleton nodes with correct rotation when flycam is rotated in XY viewport.", ({ + it("should create skeleton nodes with correct rotation when flycam is rotated in all three viewports.", ({ api, }) => { - const rotation = [20, 90, 10] as Vector3; - const rotationQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...toRadian(rotation)), - ); - Store.dispatch(setRotationAction(rotation)); - Store.dispatch(setViewportAction(OrthoViews.PLANE_XY)); - api.tracing.createNode([10, 10, 10]); - const newNode = enforceSkeletonTracing(Store.getState().annotation) - .trees.getOrThrow(2) - .nodes.getOrThrow(4); - const newNodeQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...toRadian(newNode.rotation)), - ); - expect(rotationQuaternion.angleTo(newNodeQuaternion)).toBeLessThan(0.000001); - }); - it("should create skeleton nodes with correct rotation when flycam is rotated in YZ viewport.", ({ - api, - }) => { - const rotation = [20, 90, 0] as Vector3; - const rotationQuaternion = new THREE.Quaternion() - .setFromEuler(new THREE.Euler(...toRadian([-rotation[0], -rotation[1], -rotation[2]]), "ZYX")) - // Apply viewport's default rotation. - .multiply(new THREE.Quaternion().setFromEuler(OrthoBaseRotations[OrthoViews.PLANE_YZ])); - Store.dispatch(setRotationAction(rotation)); - Store.dispatch(setViewportAction(OrthoViews.PLANE_YZ)); - api.tracing.createNode([10, 10, 10]); - const newNode = enforceSkeletonTracing(Store.getState().annotation) - .trees.getOrThrow(2) - .nodes.getOrThrow(4); - const newNodeQuaternion = new THREE.Quaternion().setFromEuler( - new THREE.Euler(...toRadian(newNode.rotation)), - ); - expect(rotationQuaternion.angleTo(newNodeQuaternion)).toBeLessThan(0.000001); + for (const testRotation of testRotations) { + for (const planeId of OrthoViewValuesWithoutTDView) { + const rotationInRadian = toRadian(testRotation); + // eulerAngleToReducerInternalMatrix and reducerInternalMatrixToEulerAngle are tested in rotation_helpers.spec.ts. + // Calculate expected rotation and make it a quaternion for equal comparison. + const rotationMatrix = eulerAngleToReducerInternalMatrix(rotationInRadian); + const rotationMatrixWithViewport = rotationMatrix.multiply( + new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[planeId]), + ); + const resultingAngle = reducerInternalMatrixToEulerAngle(rotationMatrixWithViewport); + const rotationQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...resultingAngle), + ); + // Test node creation. + Store.dispatch(setRotationAction(testRotation)); + Store.dispatch(setViewportAction(planeId)); + api.tracing.createNode([10, 10, 10], { activate: true }); + const { activeTreeId, activeNodeId } = Store.getState().annotation.skeleton || { + activeTreeId: null, + activeNodeId: null, + }; + expect(activeTreeId).not.toBeNull(); + expect(activeNodeId).not.toBeNull(); + if (!activeNodeId || !activeTreeId) { + throw new Error("Satisfy TS. The assertion above should already report this."); + } + console.error({ activeTreeId, activeNodeId }); + const newNode = enforceSkeletonTracing(Store.getState().annotation) + .trees.getOrThrow(activeTreeId) + .nodes.getOrThrow(activeNodeId); + const newNodeQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...toRadian(newNode.rotation)), + ); + expect( + rotationQuaternion.angleTo(newNodeQuaternion), + `Node rotation ${newNode.rotation} is not nearly equal to ${map3(THREE.MathUtils.radToDeg, resultingAngle)} in viewport ${planeId}.`, + ).toBeLessThan(0.000001); + } + } }); }); diff --git a/frontend/javascripts/test/fixtures/test_rotations.ts b/frontend/javascripts/test/fixtures/test_rotations.ts new file mode 100644 index 00000000000..349c792664e --- /dev/null +++ b/frontend/javascripts/test/fixtures/test_rotations.ts @@ -0,0 +1,15 @@ +import type { Vector3 } from "viewer/constants"; + +const testRotations: Vector3[] = [ + [0, 0, 0], + [10, 10, 10], + [90, 160, 90], + [30, 90, 40], + [90, 90, 90], + [180, 180, 180], + [30, 30, 30], + [90, 90, 188], + [333, 222, 111], + [73, 400, 666], +]; +export default testRotations; diff --git a/frontend/javascripts/test/misc/rotation_helpers.spec.ts b/frontend/javascripts/test/misc/rotation_helpers.spec.ts new file mode 100644 index 00000000000..63337fc0e90 --- /dev/null +++ b/frontend/javascripts/test/misc/rotation_helpers.spec.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from "vitest"; +import { + eulerAngleToReducerInternalMatrix, + reducerInternalMatrixToEulerAngle, +} from "viewer/model/helpers/rotation_helpers"; +import type { Vector3 } from "viewer/constants"; +import * as THREE from "three"; +import { map3 } from "libs/utils"; +import testRotations from "test/fixtures/test_rotations"; + +describe("Rotation Helper Functions", () => { + it("should result in an equal rotation after transforming into flycam reducer rotation space and back.", () => { + for (const testRotation of testRotations) { + const inputRotationInRadian = map3(THREE.MathUtils.degToRad, testRotation); + const rotationMatrix = eulerAngleToReducerInternalMatrix(inputRotationInRadian); + const resultingAngle = reducerInternalMatrixToEulerAngle(rotationMatrix); + const inputQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...inputRotationInRadian), + ); + const outputQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...resultingAngle), + ); + expect( + inputQuaternion.angleTo(outputQuaternion), + `Angle ${testRotation} is not converted properly`, + ).toBeLessThan(0.000001); + } + }); + // This tests goal is to test and document that the output after converting back from the 'flycam rotation reducer space' the result needs to be interpreted as a XYZ Euler angle. + it("should *not* result in an equal rotation after transforming into flycam reducer rotation space and back if interpreted in wrong euler order.", () => { + const testRotation = [30, 90, 40] as Vector3; + const inputRotationInRadian = map3(THREE.MathUtils.degToRad, testRotation); + const rotationMatrix = eulerAngleToReducerInternalMatrix(inputRotationInRadian); + const resultingAngle = reducerInternalMatrixToEulerAngle(rotationMatrix); + const inputQuaternionZYX = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...inputRotationInRadian, "ZYX"), + ); + const outputQuaternionZYX = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...resultingAngle, "ZYX"), + ); + expect( + inputQuaternionZYX.angleTo(outputQuaternionZYX), + `Angle ${testRotation} is equal although interpreted as 'ZYX' euler order. `, + ).toBeGreaterThan(0.001); + const inputQuaternionYZX = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...inputRotationInRadian, "YZX"), + ); + const outputQuaternionYZX = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...resultingAngle, "YZX"), + ); + expect( + inputQuaternionYZX.angleTo(outputQuaternionYZX), + `Angle ${testRotation} is equal although interpreted as 'YZX' euler order. `, + ).toBeGreaterThan(0.001); + const inputQuaternionXYZ = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...inputRotationInRadian, "XYZ"), + ); + const outputQuaternionXYZ = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...resultingAngle, "XYZ"), + ); + expect( + inputQuaternionXYZ.angleTo(outputQuaternionXYZ), + `Angle ${testRotation} is not equal although interpreted as 'XYZ' euler order should be correct. `, + ).toBeLessThan(0.00001); + }); +}); diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index 30c0588b804..6da8c775542 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -350,13 +350,6 @@ export function getFlycamRotationWithAppendedRotation( rotationToAppend: THREE.Euler, ): Vector3 { const flycamRotation = getRotationInRadian(flycam, false); - if (V3.equals(flycamRotation, [0, 0, 0])) { - return map3(THREE.MathUtils.radToDeg, [ - rotationToAppend.x, - rotationToAppend.y, - rotationToAppend.z, - ]); - } // Perform same operations as the flycam reducer does. First default 180° around z. let rotFlycamMatrix = eulerAngleToReducerInternalMatrix(flycamRotation); From 99322d585ac0647c834d40fa3857f84bc2246470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:52:12 +0200 Subject: [PATCH 112/128] remove outdated todo comments --- .../viewer/geometries/materials/plane_material_factory.ts | 1 - .../viewer/view/action-bar/dataset_rotation_popover_view.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts index cd50447fdf4..83d06b0dea5 100644 --- a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts +++ b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts @@ -597,7 +597,6 @@ class PlaneMaterialFactory { }, ), listenToStoreProperty( - // TODOM: Fix rotation direction. Seems to be in different direction than actual rotation. (storeState) => getRotationInRadian(storeState.flycam), (rotation) => { const state = Store.getState(); diff --git a/frontend/javascripts/viewer/view/action-bar/dataset_rotation_popover_view.tsx b/frontend/javascripts/viewer/view/action-bar/dataset_rotation_popover_view.tsx index de101de6f8f..868b9baa2b9 100644 --- a/frontend/javascripts/viewer/view/action-bar/dataset_rotation_popover_view.tsx +++ b/frontend/javascripts/viewer/view/action-bar/dataset_rotation_popover_view.tsx @@ -15,8 +15,6 @@ export const warningColors: React.CSSProperties = { borderColor: "rgb(241, 122, 39)", }; -// TODO: write tests for reproducable - const PopoverContent: React.FC = () => { const rotation = useWkSelector((state) => state.flycam.rotation); const handleChangeRotation = useCallback((rotation: Vector3) => { From d3aa4ff6eff5b751cf6d8ad04d3b6a067d4df352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:39:27 +0200 Subject: [PATCH 113/128] fix & unify skeleton node creation tests --- .../test/api/api_skeleton_latest.spec.ts | 150 +++++++----------- 1 file changed, 60 insertions(+), 90 deletions(-) diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts index b40ae580653..c82792f977d 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts @@ -29,6 +29,20 @@ const toRadian = (arr: Vector3): Vector3 => [ THREE.MathUtils.degToRad(arr[2]), ]; +function applyRotationInFlycamReducerSpace( + flycamRotationInRadian: Vector3, + rotationToApply: THREE.Euler, +): Vector3 { + // eulerAngleToReducerInternalMatrix and reducerInternalMatrixToEulerAngle are tested in rotation_helpers.spec.ts. + // Calculate expected rotation and make it a quaternion for equal comparison. + const rotationMatrix = eulerAngleToReducerInternalMatrix(flycamRotationInRadian); + const rotationMatrixWithViewport = rotationMatrix.multiply( + new THREE.Matrix4().makeRotationFromEuler(rotationToApply), + ); + const resultingAngle = reducerInternalMatrixToEulerAngle(rotationMatrixWithViewport); + return resultingAngle; +} + describe("API Skeleton", () => { beforeEach(async (context) => { await setupWebknossosForTesting(context, "skeleton", { dontDispatchWkReady: true }); @@ -319,78 +333,45 @@ describe("API Skeleton", () => { true, ); }); - it("should create skeleton nodes with correct properties for XY viewport", ({ - api, - }) => { - Store.dispatch(setRotationAction([0, 0, 0])); - Store.dispatch(setViewportAction(OrthoViews.PLANE_XY)); - api.tracing.createNode([10, 10, 10]); - const newNode = enforceSkeletonTracing(Store.getState().annotation) - .trees.getOrThrow(2) - .nodes.getOrThrow(4); - const propsToCheck = { - untransformedPosition: newNode.untransformedPosition, - additionalCoordinates: newNode.additionalCoordinates, - rotation: newNode.rotation, - viewport: newNode.viewport, - mag: newNode.mag, - }; - expect(propsToCheck).toStrictEqual({ - untransformedPosition: [10, 10, 10], - additionalCoordinates: [], - rotation: [0, 0, 0], - viewport: OrthoViewToNumber[OrthoViews.PLANE_XY], - mag: 0, - }); - }); - it("should create skeleton nodes with correct properties for YZ viewport", ({ - api, - }) => { - Store.dispatch(setRotationAction([0, 0, 0])); - Store.dispatch(setViewportAction(OrthoViews.PLANE_YZ)); - api.tracing.createNode([10, 10, 10]); - const newNode = enforceSkeletonTracing(Store.getState().annotation) - .trees.getOrThrow(2) - .nodes.getOrThrow(4); - const propsToCheck = { - untransformedPosition: newNode.untransformedPosition, - additionalCoordinates: newNode.additionalCoordinates, - rotation: newNode.rotation, - viewport: newNode.viewport, - mag: newNode.mag, - }; - expect(propsToCheck).toStrictEqual({ - untransformedPosition: [10, 10, 10], - additionalCoordinates: [], - rotation: [0, 270, 0], - viewport: OrthoViewToNumber[OrthoViews.PLANE_YZ], - mag: 0, - }); - }); - it("should create skeleton nodes with correct properties for XZ viewport", ({ - api, - }) => { - Store.dispatch(setRotationAction([0, 0, 0])); - Store.dispatch(setViewportAction(OrthoViews.PLANE_XZ)); - api.tracing.createNode([10, 10, 10]); - const newNode = enforceSkeletonTracing(Store.getState().annotation) - .trees.getOrThrow(2) - .nodes.getOrThrow(4); - const propsToCheck = { - untransformedPosition: newNode.untransformedPosition, - additionalCoordinates: newNode.additionalCoordinates, - rotation: newNode.rotation, - viewport: newNode.viewport, - mag: newNode.mag, - }; - expect(propsToCheck).toStrictEqual({ - untransformedPosition: [10, 10, 10], - additionalCoordinates: [], - rotation: [90, 0, 0], - viewport: OrthoViewToNumber[OrthoViews.PLANE_XZ], - mag: 0, - }); + it("should create skeleton nodes with correct properties.", ({ api }) => { + const flycamRotation = [0, 0, 0] as Vector3; + for (const planeId of OrthoViewValuesWithoutTDView) { + const rotationForComparison = applyRotationInFlycamReducerSpace( + flycamRotation, + OrthoBaseRotations[planeId], + ); + const rotationQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...rotationForComparison), + ); + Store.dispatch(setRotationAction(flycamRotation)); + Store.dispatch(setViewportAction(planeId)); + api.tracing.createNode([10, 10, 10], { activate: true }); + const skeletonTracing = enforceSkeletonTracing(Store.getState().annotation); + // Throw error if no node / tree is active by passing -1 as id. + const newNode = skeletonTracing.trees + .getOrThrow(skeletonTracing.activeTreeId || -1) + .nodes.getOrThrow(skeletonTracing.activeNodeId || -1); + const propsToCheck = { + untransformedPosition: newNode.untransformedPosition, + additionalCoordinates: newNode.additionalCoordinates, + viewport: newNode.viewport, + mag: newNode.mag, + }; + expect(propsToCheck).toStrictEqual({ + untransformedPosition: [10, 10, 10], + additionalCoordinates: [], + viewport: OrthoViewToNumber[planeId], + mag: 0, + }); + const newNodeQuaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(...toRadian(newNode.rotation)), + ); + expect( + rotationQuaternion.angleTo(newNodeQuaternion), + `Node rotation ${newNode.rotation} is not nearly equal to ${map3(THREE.MathUtils.radToDeg, rotationForComparison)} in viewport ${planeId}.`, + ).toBeLessThan(0.000001); + } }); it("should create skeleton nodes with correct rotation when flycam is rotated in all three viewports.", ({ api, @@ -398,13 +379,10 @@ describe("API Skeleton", () => { for (const testRotation of testRotations) { for (const planeId of OrthoViewValuesWithoutTDView) { const rotationInRadian = toRadian(testRotation); - // eulerAngleToReducerInternalMatrix and reducerInternalMatrixToEulerAngle are tested in rotation_helpers.spec.ts. - // Calculate expected rotation and make it a quaternion for equal comparison. - const rotationMatrix = eulerAngleToReducerInternalMatrix(rotationInRadian); - const rotationMatrixWithViewport = rotationMatrix.multiply( - new THREE.Matrix4().makeRotationFromEuler(OrthoBaseRotations[planeId]), + const resultingAngle = applyRotationInFlycamReducerSpace( + rotationInRadian, + OrthoBaseRotations[planeId], ); - const resultingAngle = reducerInternalMatrixToEulerAngle(rotationMatrixWithViewport); const rotationQuaternion = new THREE.Quaternion().setFromEuler( new THREE.Euler(...resultingAngle), ); @@ -412,19 +390,11 @@ describe("API Skeleton", () => { Store.dispatch(setRotationAction(testRotation)); Store.dispatch(setViewportAction(planeId)); api.tracing.createNode([10, 10, 10], { activate: true }); - const { activeTreeId, activeNodeId } = Store.getState().annotation.skeleton || { - activeTreeId: null, - activeNodeId: null, - }; - expect(activeTreeId).not.toBeNull(); - expect(activeNodeId).not.toBeNull(); - if (!activeNodeId || !activeTreeId) { - throw new Error("Satisfy TS. The assertion above should already report this."); - } - console.error({ activeTreeId, activeNodeId }); - const newNode = enforceSkeletonTracing(Store.getState().annotation) - .trees.getOrThrow(activeTreeId) - .nodes.getOrThrow(activeNodeId); + const skeletonTracing = enforceSkeletonTracing(Store.getState().annotation); + // Throw error if no node / tree is active by passing -1 as id. + const newNode = skeletonTracing.trees + .getOrThrow(skeletonTracing.activeTreeId || -1) + .nodes.getOrThrow(skeletonTracing.activeNodeId || -1); const newNodeQuaternion = new THREE.Quaternion().setFromEuler( new THREE.Euler(...toRadian(newNode.rotation)), ); From 5ce70c1c765f221fe166749086a7d891aac726da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:31:36 +0200 Subject: [PATCH 114/128] add doc comments --- frontend/javascripts/viewer/constants.ts | 3 +++ .../viewer/model/accessors/flycam_accessor.ts | 12 +++++++----- .../viewer/model/helpers/rotation_helpers.ts | 2 ++ .../viewer/model/reducers/flycam_reducer.ts | 2 ++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/viewer/constants.ts b/frontend/javascripts/viewer/constants.ts index af7ff3a451b..1f73f93a136 100644 --- a/frontend/javascripts/viewer/constants.ts +++ b/frontend/javascripts/viewer/constants.ts @@ -128,6 +128,9 @@ export const OrthoViewCrosshairColors: OrthoViewMap<[number, number]> = { [OrthoViews.PLANE_XZ]: [BLUE, PINK], [OrthoViews.TDView]: [0x000000, 0x000000], }; + +// See the following or an explanation about the relative orientation of the viewports toward the XY viewport. +// https://www.notion.so/scalableminds/3D-Rotations-3D-Scene-210b51644c6380c2a4a6f5f3c069738a?source=copy_link#22bb51644c63800e8682e92a5c91a519 export const OrthoBaseRotations = { [OrthoViews.PLANE_XY]: new THREE.Euler(0, 0, 0), [OrthoViews.PLANE_YZ]: new THREE.Euler(0, (3 / 2) * Math.PI, 0), diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index 6da8c775542..7698d6c3baa 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -306,7 +306,8 @@ const flycamMatrixObject = new THREE.Matrix4(); // Returns the current rotation of the flycam in radians as an euler xyz tuple. // As the order in which the angles are applied is zyx (see flycam_reducer setRotationReducer), -// this order must be followed when this euler angle is applied to 2d computations. +// this order must be followed when this euler angle is applied to 3d rotation computations. +// But when calculating with function _getRotationInRadianFromMatrix(flycamMatrix: Matrix4x4, invertZ: boolean = true): Vector3 { // Somehow z rotation is inverted but the others are not. const zInvertFactor = invertZ ? -1 : 1; @@ -343,17 +344,15 @@ function _isRotated(flycam: Flycam): boolean { return !V3.equals(getRotationInRadian(flycam), [0, 0, 0]); } -// Memoizing this function makes no sense as its result will always be used to change the flycam rotation. -export function getFlycamRotationWithAppendedRotation( +function _getFlycamRotationWithAppendedRotation( flycam: Flycam, - // prependedRotation must be in ZYX order. rotationToAppend: THREE.Euler, ): Vector3 { const flycamRotation = getRotationInRadian(flycam, false); // Perform same operations as the flycam reducer does. First default 180° around z. let rotFlycamMatrix = eulerAngleToReducerInternalMatrix(flycamRotation); - // Apply viewport default rotation + // Apply rotation rotFlycamMatrix = rotFlycamMatrix.multiply( new THREE.Matrix4().makeRotationFromEuler(rotationToAppend), ); @@ -372,6 +371,9 @@ export const getPosition = memoizeOne(_getPosition); export const getFlooredPosition = memoizeOne(_getFlooredPosition); export const getRotationInRadian = memoizeOne(_getRotationInRadian); export const getRotationInDegrees = memoizeOne(_getRotationInDegrees); +export const getFlycamRotationWithAppendedRotation = memoizeOne( + _getFlycamRotationWithAppendedRotation, +); export const isRotated = memoizeOne(_isRotated); export const getZoomedMatrix = memoizeOne(_getZoomedMatrix); diff --git a/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts b/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts index 519c395ca67..306afa035bb 100644 --- a/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts +++ b/frontend/javascripts/viewer/model/helpers/rotation_helpers.ts @@ -13,6 +13,8 @@ const invertedEulerMatrix = new THREE.Matrix4(); // first convert the angle into the quirky way the flycam matrix does rotations. // After the rotation calculation is done, the companion function reducerInternalMatrixToEulerAngle // should be used to transform the result back. +// For some more info look at +// https://www.notion.so/scalableminds/3D-Rotations-3D-Scene-210b51644c6380c2a4a6f5f3c069738a?source=copy_link#22bb51644c6380cf8302fb8f6749ae1d. export function eulerAngleToReducerInternalMatrix(angleInRadian: Vector3): THREE.Matrix4 { // Perform same operations as the flycam reducer does. First default 180° around z. let matrixLikeInReducer = matrix.makeRotationZ(Math.PI); diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index c377d4fae38..4f0bfe7140a 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -176,6 +176,8 @@ export function setDirectionReducer(state: WebknossosState, direction: Vector3) }); } +// The way rotations are currently handled / interpreted is quirky. See here for more information: +// https://www.notion.so/scalableminds/3D-Rotations-3D-Scene-210b51644c6380c2a4a6f5f3c069738a?source=copy_link#22bb51644c63800fb874e717e49da7bc export function setRotationReducer(state: WebknossosState, rotation: Vector3) { if (state.dataset != null) { const [x, y, z] = rotation; From c721989f9166f2b18dde94d254710ddeae208a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:32:39 +0200 Subject: [PATCH 115/128] add keyboard shortcuts docs --- docs/ui/keyboard_shortcuts.md | 43 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/docs/ui/keyboard_shortcuts.md b/docs/ui/keyboard_shortcuts.md index df83924e80d..9510a3792ed 100644 --- a/docs/ui/keyboard_shortcuts.md +++ b/docs/ui/keyboard_shortcuts.md @@ -40,27 +40,28 @@ A complete listing of all available keyboard & mouse shortcuts for WEBKNOSSOS ca Note that skeleton-specific mouse actions are usually only available when the skeleton tool is active. -| Key Binding | Operation | -| --------------------------------------------- | ------------------------------------------- | -| Left Mouse Drag or Arrow Keys | Move In-Plane | -| ++alt++ + Mouse Move | Move In-Plane | -| ++space++ | Move Forward | -| Scroll Mousewheel (3D View) | Zoom In And Out | -| Right-Click Drag (3D View) | Rotate 3D View | -| Left Click | Create New Node | -| Left Click | Select Node (Mark as Active Node) under cursor | -| Left Drag | Move node under cursor | -| Right Click (on node) | Bring up the context-menu with further actions | -| ++shift++ + ++alt++ + Left Click | Merge Two Nodes and Combine Trees | -| ++shift++ + ++ctrl++ / ++cmd++ + Left Click | Delete Edge / Split Trees | -| ++c++ | Create New Tree | -| ++ctrl++ / ++cmd++ + ++period++ | Navigate to the next Node (Mark as Active) | -| ++ctrl++ / ++cmd++ + ++comma++ | Navigate to previous Node (Mark as Active) | -| ++ctrl++ / ++cmd++ + Left Click or ++ctrl++ / ++cmd++ + Arrow Keys | Move the Active Node | -| ++del++ | Delete Node / Split Trees | -| ++b++ | Mark Node as New Branchpoint | -| ++j++ | Jump To Last Branchpoint | -| ++s++ | Center Camera on Active Node | +| Key Binding | Operation | +| -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| Left Mouse Drag or Arrow Keys | Move In-Plane | +| ++alt++ + Mouse Move | Move In-Plane | +| ++space++ | Move Forward | +| ++shift++ + ++up++ / ++down++ / ++left++ / ++right++
++alt++ + ++left++ / ++right++ | Rotate Planes | +| ++ctrl++ / ++cmd++ + ++shift++ + ++up++ / ++down++ / ++left++ / ++right++
++ctrl++ / ++cmd++ + ++alt++ + ++left++ / ++right++ | Rotate Planes by 90° | +| Right-Click Drag (3D View) | Rotate 3D View | +| Left Click | Create New Node | +| Left Click | Select Node (Mark as Active Node) under cursor | +| Left Drag | Move node under cursor | +| Right Click (on node) | Bring up the context-menu with further actions | +| ++shift++ + ++alt++ + Left Click | Merge Two Nodes and Combine Trees | +| ++shift++ + ++ctrl++ / ++cmd++ + Left Click | Delete Edge / Split Trees | +| ++c++ | Create New Tree | +| ++ctrl++ / ++cmd++ + ++period++ | Navigate to the next Node (Mark as Active) | +| ++ctrl++ / ++cmd++ + ++comma++ | Navigate to previous Node (Mark as Active) | +| ++ctrl++ / ++cmd++ + Left Click or ++ctrl++ / ++cmd++ + Arrow Keys | Move the Active Node | +| ++del++ | Delete Node / Split Trees | +| ++b++ | Mark Node as New Branchpoint | +| ++j++ | Jump To Last Branchpoint | +| ++s++ | Center Camera on Active Node | Note that you can enable *Classic Controls* which will behave slightly different and more explicit for the mouse actions: From 18a9221fe263a98333b27c120207403c2712f04f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:37:13 +0200 Subject: [PATCH 116/128] remove unused import --- frontend/javascripts/test/api/api_skeleton_latest.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts index c82792f977d..1b2e6707c93 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.ts +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.ts @@ -7,7 +7,6 @@ import Store from "viewer/store"; import { vi, describe, it, expect, beforeEach } from "vitest"; import { OrthoBaseRotations, - OrthoViews, OrthoViewToNumber, OrthoViewValuesWithoutTDView, type Vector3, From ef2b1c0afa7e9f197f8d361183db10db926f7f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:49:49 +0200 Subject: [PATCH 117/128] fix distance measurement tooltip to stay active in an anisotropic scaled dataset --- .../model/accessors/view_mode_accessor.ts | 58 +++++++++++++------ .../view/distance_measurement_tooltip.tsx | 51 +++++++++++----- 2 files changed, 74 insertions(+), 35 deletions(-) diff --git a/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts b/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts index e6fadf51cae..914d52a710b 100644 --- a/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts @@ -99,7 +99,7 @@ const flycamRotationEuler = new THREE.Euler(); const flycamRotationMatrix = new THREE.Matrix4(); const flycamPositionMatrix = new THREE.Matrix4(); const rotatedDiff = new THREE.Vector3(); -const planeRationVector = new THREE.Vector3(); +const planeRatioVector = new THREE.Vector3(); function _calculateMaybeGlobalPos( state: WebknossosState, @@ -122,7 +122,7 @@ function _calculateMaybeGlobalPos( flycamPositionMatrix.makeTranslation(...curGlobalPos); rotatedDiff.set(...diffXyz).applyMatrix4(flycamRotationMatrix); const scaledRotatedPosition = rotatedDiff - .multiply(planeRationVector.set(...planeRatio)) + .multiply(planeRatioVector.set(...planeRatio)) .multiplyScalar(-1); const globalFloatingPosition = scaledRotatedPosition.applyMatrix4(flycamPositionMatrix); @@ -163,6 +163,32 @@ function _calculateMaybeGlobalPos( return { rounded: roundedPosition, floating: floatingPosition }; } +// This function inverts parts of the _calculateMaybeGlobalPos function. +// It takes a global position and calculates a screen space vector relative to the flycam position for it. +// The result it like the input of position of _calculateMaybeGlobalPos but as a 3D vector from which the +// viewport dependant coordinates need to be extracted (xy -> xy, yz -> zy, xz -> xz). +function _calculateInViewportPos( + globalPosition: Vector3, + flycamPosition: Vector3, + flycamRotationInRadian: Vector3, + planeRatio: Vector3, + zoomStep: number, +): THREE.Vector3 { + // Difference in world space + const positionDiff = new THREE.Vector3(...V3.sub(globalPosition, flycamPosition)); + + // Inverse rotate the world difference vector into local plane-aligned space + const inverseRotationMatrix = new THREE.Matrix4() + .makeRotationFromEuler(new THREE.Euler(...flycamRotationInRadian, "ZYX")) + .invert(); + + // Unscale from voxel ratio (undo voxel scaling) + const posInScreenSpaceScaling = positionDiff.divide(new THREE.Vector3(...planeRatio)); + const rotatedIntoScreenSpace = posInScreenSpaceScaling.applyMatrix4(inverseRotationMatrix); + const unzoomedPosition = rotatedIntoScreenSpace.multiplyScalar(1 / zoomStep); + return unzoomedPosition; +} + function _calculateMaybePlaneScreenPos( state: WebknossosState, globalPosition: Vector3, @@ -178,35 +204,28 @@ function _calculateMaybePlaneScreenPos( const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); const navbarHeight = state.uiInformation.navbarHeight; - // Difference in world space - const positionDiff = new THREE.Vector3(...V3.sub(globalPosition, flycamPosition)); - - // Inverse rotate the world difference vector into local plane-aligned space - const inverseRotationMatrix = new THREE.Matrix4() - .makeRotationFromEuler(new THREE.Euler(...flycamRotation, "ZYX")) - .invert(); - - const localDiff = positionDiff.applyMatrix4(inverseRotationMatrix); - - // Unscale from voxel ratio (undo voxel scaling) - const scaledLocalDiff = localDiff - .divide(new THREE.Vector3(...planeRatio)) - .multiplyScalar(1 / state.flycam.zoomStep); + const positionInViewportPerspective = calculateInViewportPos( + globalPosition, + flycamPosition, + flycamRotation, + planeRatio, + state.flycam.zoomStep, + ); // Get plane-aligned screen-space coordinates (u/v) switch (planeId) { case OrthoViews.PLANE_XY: { - point = [scaledLocalDiff.x, scaledLocalDiff.y]; + point = [positionInViewportPerspective.x, positionInViewportPerspective.y]; break; } case OrthoViews.PLANE_YZ: { - point = [scaledLocalDiff.z, scaledLocalDiff.y]; + point = [positionInViewportPerspective.z, positionInViewportPerspective.y]; break; } case OrthoViews.PLANE_XZ: { - point = [scaledLocalDiff.x, scaledLocalDiff.z]; + point = [positionInViewportPerspective.x, positionInViewportPerspective.z]; break; } @@ -319,6 +338,7 @@ export const calculateMaybeGlobalPos = reuseInstanceOnEquality(_calculateMaybeGl export const calculateGlobalPos = reuseInstanceOnEquality(_calculateGlobalPos); export const calculateGlobalDelta = reuseInstanceOnEquality(_calculateGlobalDelta); export const calculateMaybePlaneScreenPos = reuseInstanceOnEquality(_calculateMaybePlaneScreenPos); +export const calculateInViewportPos = reuseInstanceOnEquality(_calculateInViewportPos); export function getViewMode(state: WebknossosState): ViewMode { return state.temporaryConfiguration.viewMode; } diff --git a/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx b/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx index dd45be1d097..e216b2ae61c 100644 --- a/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx +++ b/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx @@ -7,22 +7,22 @@ import { formatNumberToArea, formatNumberToLength, } from "libs/format_utils"; -import { V3 } from "libs/mjs"; import { useWkSelector } from "libs/react_hooks"; import { clamp } from "libs/utils"; import { useEffect, useRef } from "react"; import { useDispatch } from "react-redux"; -import * as THREE from "three"; import { LongUnitToShortUnitMap, type OrthoView, type Vector3 } from "viewer/constants"; import getSceneController from "viewer/controller/scene_controller_provider"; import { getPosition, getRotationInRadian } from "viewer/model/accessors/flycam_accessor"; import { AnnotationTool, MeasurementTools } from "viewer/model/accessors/tool_accessor"; import { + calculateInViewportPos, calculateMaybePlaneScreenPos, getInputCatcherRect, } from "viewer/model/accessors/view_mode_accessor"; import { hideMeasurementTooltipAction } from "viewer/model/actions/ui_actions"; import Dimensions from "viewer/model/dimensions"; +import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; const TOOLTIP_HEIGHT = 48; const ADDITIONAL_OFFSET = 12; @@ -47,16 +47,18 @@ function isPositionStillInPlane( flycamRotation: Vector3, flycamPosition: Vector3, planeId: OrthoView, + baseVoxelFactors: Vector3, + zoomStep: number, ) { - const inverseFlycamRotationMatrix = new THREE.Matrix4().makeRotationFromEuler( - // As we apply the inverse of the euler angle the flycam currently has we need to invert the order as well. - new THREE.Euler(...V3.scale(flycamRotation, -1), "XYZ"), - ); - const positionUvw = new THREE.Vector3(...V3.sub(positionXYZ, flycamPosition)) - .applyMatrix4(inverseFlycamRotationMatrix) - .toArray(); + const posInViewport = calculateInViewportPos( + positionXYZ, + flycamPosition, + flycamRotation, + baseVoxelFactors, + zoomStep, + ).toArray(); const thirdDim = Dimensions.thirdDimensionForPlane(planeId); - return Math.abs(positionUvw[thirdDim]) < 1; + return Math.abs(posInViewport[thirdDim]) < 1; } export default function DistanceMeasurementTooltip() { @@ -64,13 +66,16 @@ export default function DistanceMeasurementTooltip() { (state) => state.uiInformation.measurementToolInfo.lastMeasuredPosition, ); const isMeasuring = useWkSelector((state) => state.uiInformation.measurementToolInfo.isMeasuring); - const flycam = useWkSelector((state) => state.flycam); + const flycamPosition = useWkSelector((state) => getPosition(state.flycam)); + const flycamRotation = useWkSelector((state) => getRotationInRadian(state.flycam)); + const zoomStep = useWkSelector((state) => state.flycam.zoomStep); const activeTool = useWkSelector((state) => state.uiInformation.activeTool); const voxelSize = useWkSelector((state) => state.dataset.dataSource.scale); + const planeRatio = useWkSelector((state) => + getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), + ); const tooltipRef = useRef(null); const dispatch = useDispatch(); - const currentPosition = getPosition(flycam); - const rotation = getRotationInRadian(flycam); const { areaMeasurementGeometry, lineMeasurementGeometry } = getSceneController(); const activeGeometry = activeTool === AnnotationTool.LINE_MEASUREMENT @@ -92,16 +97,30 @@ export default function DistanceMeasurementTooltip() { // biome-ignore lint/correctness/useExhaustiveDependencies(hideMeasurementTooltipAction): constant // biome-ignore lint/correctness/useExhaustiveDependencies(dispatch): constant - // biome-ignore lint/correctness/useExhaustiveDependencies(activeGeometry.resetAndHide): useEffect(() => { if ( lastMeasuredGlobalPosition && - !isPositionStillInPlane(lastMeasuredGlobalPosition, rotation, currentPosition, orthoView) + !isPositionStillInPlane( + lastMeasuredGlobalPosition, + flycamRotation, + flycamPosition, + orthoView, + planeRatio, + zoomStep, + ) ) { dispatch(hideMeasurementTooltipAction()); activeGeometry.resetAndHide(); } - }, [lastMeasuredGlobalPosition, rotation, currentPosition, orthoView]); + }, [ + lastMeasuredGlobalPosition, + flycamRotation, + flycamPosition, + orthoView, + planeRatio, + zoomStep, + activeGeometry.resetAndHide, + ]); if ( lastMeasuredGlobalPosition == null || From 719d35f8a37a20373b7f1219ef50b52e8f76e145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:50:50 +0200 Subject: [PATCH 118/128] move flycam accessors spec --- .../test/model/{ => accessors}/flycam_accessors.spec.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/javascripts/test/model/{ => accessors}/flycam_accessors.spec.ts (100%) diff --git a/frontend/javascripts/test/model/flycam_accessors.spec.ts b/frontend/javascripts/test/model/accessors/flycam_accessors.spec.ts similarity index 100% rename from frontend/javascripts/test/model/flycam_accessors.spec.ts rename to frontend/javascripts/test/model/accessors/flycam_accessors.spec.ts From 06e6cc684e7044ac4d23f9f040dfd7ce8081a827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:38:02 +0200 Subject: [PATCH 119/128] add tests for calculateMaybeGlobalPos as well as calculating the result back with calculateInViewportPos --- .../test/fixtures/test_rotations.ts | 1 + .../test/libs/transform_spec_helpers.ts | 7 +- .../accessors/view_mode_accessors.spec.ts | 358 ++++++++++++++++++ 3 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 frontend/javascripts/test/model/accessors/view_mode_accessors.spec.ts diff --git a/frontend/javascripts/test/fixtures/test_rotations.ts b/frontend/javascripts/test/fixtures/test_rotations.ts index 349c792664e..bb599f9efc6 100644 --- a/frontend/javascripts/test/fixtures/test_rotations.ts +++ b/frontend/javascripts/test/fixtures/test_rotations.ts @@ -2,6 +2,7 @@ import type { Vector3 } from "viewer/constants"; const testRotations: Vector3[] = [ [0, 0, 0], + [83, 0, 0], [10, 10, 10], [90, 160, 90], [30, 90, 40], diff --git a/frontend/javascripts/test/libs/transform_spec_helpers.ts b/frontend/javascripts/test/libs/transform_spec_helpers.ts index 318f85d9bbc..c0edf150df5 100644 --- a/frontend/javascripts/test/libs/transform_spec_helpers.ts +++ b/frontend/javascripts/test/libs/transform_spec_helpers.ts @@ -6,14 +6,15 @@ export function almostEqual( vec1: Vector3, vec2: Vector3, threshold: number = 1, + message?: string, ) { const diffX = Math.abs(vec1[0] - vec2[0]); const diffY = Math.abs(vec1[1] - vec2[1]); const diffZ = Math.abs(vec1[2] - vec2[2]); - expect(diffX).toBeLessThan(threshold); - expect(diffY).toBeLessThan(threshold); - expect(diffZ).toBeLessThan(threshold); + expect(diffX, message).toBeLessThan(threshold); + expect(diffY, message).toBeLessThan(threshold); + expect(diffZ, message).toBeLessThan(threshold); } export function getPointsC555() { diff --git a/frontend/javascripts/test/model/accessors/view_mode_accessors.spec.ts b/frontend/javascripts/test/model/accessors/view_mode_accessors.spec.ts new file mode 100644 index 00000000000..cfa15f3236f --- /dev/null +++ b/frontend/javascripts/test/model/accessors/view_mode_accessors.spec.ts @@ -0,0 +1,358 @@ +import update from "immutability-helper"; +import { describe, it, expect } from "vitest"; +import type { WebknossosState } from "viewer/store"; +import * as accessors from "viewer/model/accessors/view_mode_accessor"; +import { + OrthoViews, + OrthoViewValuesWithoutTDView, + UnitLong, + type Vector2, + type Vector3, +} from "viewer/constants"; +import defaultState from "viewer/default_state"; +import { FlycamMatrixWithDefaultRotation } from "test/fixtures/flycam_object"; +import { M4x4, V3 } from "libs/mjs"; +import Dimensions from "viewer/model/dimensions"; +import { setRotationAction } from "viewer/model/actions/flycam_actions"; +import FlycamReducer from "viewer/model/reducers/flycam_reducer"; +import * as THREE from "three"; +import { map3 } from "libs/utils"; +import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; +import { almostEqual } from "test/libs/transform_spec_helpers"; +import testRotations from "test/fixtures/test_rotations"; + +const viewportOffsets = { + left: 200, + top: 20, +}; +const viewportSize = 800; + +const viewModeDataInputCatcher = { + PLANE_XY: { + left: viewportOffsets.left, + top: viewportOffsets.top, + width: viewportSize, + height: viewportSize, + }, + PLANE_YZ: { + left: viewportOffsets.left + viewportSize, + top: viewportOffsets.top, + width: viewportSize, + height: viewportSize, + }, + PLANE_XZ: { + left: viewportOffsets.left, + top: viewportOffsets.top + viewportSize, + width: viewportSize, + height: viewportSize, + }, + TDView: { + left: viewportOffsets.left + viewportSize, + top: viewportOffsets.top + viewportSize, + width: viewportSize, + height: viewportSize, + }, +}; + +const initialFlycamPosition = [100, 100, 100] as Vector3; + +const initialState: WebknossosState = { + ...defaultState, + dataset: { + ...defaultState.dataset, + dataSource: { + ...defaultState.dataset.dataSource, + scale: { factor: [1, 1, 1], unit: UnitLong.nm }, + }, + }, + datasetConfiguration: { ...defaultState.datasetConfiguration }, + userConfiguration: { + ...defaultState.userConfiguration, + sphericalCapRadius: 100, + dynamicSpaceDirection: true, + }, + flycam: { + ...defaultState.flycam, + zoomStep: 1.0, + // Flycam per default translated by initialFlycamPosition + // and has the default rotation of 180°. For more see flycam_reducer.ts + currentMatrix: M4x4.mul( + M4x4.translate(initialFlycamPosition, M4x4.identity()), + FlycamMatrixWithDefaultRotation, + ), + spaceDirectionOrtho: [1, 1, 1], + rotation: [0, 0, 0], + }, + viewModeData: { + ...defaultState.viewModeData, + plane: { + ...defaultState.viewModeData.plane, + activeViewport: OrthoViews.PLANE_XY, + inputCatcherRects: viewModeDataInputCatcher, + }, + }, +}; + +const testOffsets = [ + [-50, 0], + [50, 0], + [0, -50], + [0, 50], + [-50, -50], + [50, 50], + [-50, 50], + [50, -50], + [-122, -200], + [-33, -88], + [33, 88], + [123, 21], + [-322, -400], + [400, 322], +]; + +describe("View mode accessors", () => { + it("should calculate the correct global position at the center of the viewports.", () => { + for (const planeId of OrthoViewValuesWithoutTDView) { + const stateWithCorrectPlaneActive = update(initialState, { + viewModeData: { plane: { activeViewport: { $set: planeId } } }, + }); + const clickPositionAtViewportCenter = { x: viewportSize / 2, y: viewportSize / 2 }; + const globalPosition = accessors.calculateGlobalPos( + stateWithCorrectPlaneActive, + clickPositionAtViewportCenter, + ); + expect(globalPosition.rounded).toStrictEqual(initialFlycamPosition); + } + }); + it("should calculate the correct global position with certain offset from the viewport centers.", () => { + for (const offset of testOffsets) { + for (const planeId of OrthoViewValuesWithoutTDView) { + const stateWithCorrectPlaneActive = update(initialState, { + viewModeData: { plane: { activeViewport: { $set: planeId } } }, + }); + const clickPositionAtViewportCenter = { + x: viewportSize / 2 + offset[0], + y: viewportSize / 2 + offset[1], + }; + const globalPosition = accessors.calculateGlobalPos( + stateWithCorrectPlaneActive, + clickPositionAtViewportCenter, + ); + const viewportAdjustedOffset = Dimensions.transDim([offset[0], offset[1], 0], planeId); + const expectedPosition = [...V3.add(initialFlycamPosition, viewportAdjustedOffset)]; + expect( + globalPosition.rounded, + `Global position is wrong with offset ${offset} in viewport ${planeId}.`, + ).toStrictEqual(expectedPosition); + } + } + }); + it("should calculate the correct global position when the flycam is rotated around the x axis by 90 degrees.", () => { + for (const offset of testOffsets) { + for (const planeId of OrthoViewValuesWithoutTDView) { + const stateWithCorrectPlaneActive = update(initialState, { + viewModeData: { plane: { activeViewport: { $set: planeId } } }, + }); + const rotateAction = setRotationAction([90, 0, 0]); + const rotatedState = FlycamReducer(stateWithCorrectPlaneActive, rotateAction); + const clickPositionAtViewportCenter = { + x: viewportSize / 2 + offset[0], + y: viewportSize / 2 + offset[1], + }; + const globalPosition = accessors.calculateGlobalPos( + rotatedState, + clickPositionAtViewportCenter, + ); + const viewportAdjustedOffset = Dimensions.transDim([offset[0], offset[1], 0], planeId); + // Rotation of 90° around the x axis swaps y and z value and inverts the y value. + const rotatedAdjustedOffset = [ + viewportAdjustedOffset[0], + -viewportAdjustedOffset[2], + viewportAdjustedOffset[1], + ] as Vector3; + const expectedPosition = [...V3.add(initialFlycamPosition, rotatedAdjustedOffset)]; + expect( + globalPosition.rounded, + `Global position is wrong with offset ${offset} in viewport ${planeId}.`, + ).toStrictEqual(expectedPosition); + } + } + }); + it("should calculate the correct global position when the flycam is rotated with an evenly scaled dataset.", () => { + for (const rotation of testRotations) { + // When using the rotation of the flycam for calculations, one has to invert the z value and interpret the resulting euler angle as ZYX. + // More info about this at https://www.notion.so/scalableminds/3D-Rotations-3D-Scene-210b51644c6380c2a4a6f5f3c069738a?source=copy_link#22bb51644c6380138fdac454d4dac2f0. + const rotationCorrected = [rotation[0], rotation[1], -rotation[2]] as Vector3; + const rotationInRadian = map3(THREE.MathUtils.degToRad, rotationCorrected); + for (const offset of testOffsets) { + const stateWithCorrectPlaneActive = update(initialState, { + viewModeData: { plane: { activeViewport: { $set: OrthoViews.PLANE_XY } } }, + }); + const rotateAction = setRotationAction(rotation); + const rotatedState = FlycamReducer(stateWithCorrectPlaneActive, rotateAction); + const clickPositionAtViewportCenter = { + x: viewportSize / 2 + offset[0], + y: viewportSize / 2 + offset[1], + }; + const globalPosition = accessors.calculateGlobalPos( + rotatedState, + clickPositionAtViewportCenter, + ); + // Applying the rotation of 83° around the x axis to the offset. + const rotatedOffset = new THREE.Vector3(offset[0], offset[1], 0) + .applyEuler(new THREE.Euler(...rotationInRadian, "ZYX")) + .toArray(); + const expectedPosition = [...V3.add(initialFlycamPosition, rotatedOffset)] as Vector3; + almostEqual( + expect, + globalPosition.floating, + expectedPosition, + 2, + `Global position is wrong with offset ${offset} and rotation ${rotation} in viewport ${OrthoViews.PLANE_XY}. Expected ${globalPosition.floating} to nearly equal ${expectedPosition}.`, + ); + } + } + }); + it("should calculate the correct global position when the flycam is rotated and the dataset has an anisotropic scale.", () => { + const anisotropicDatasetScale = [11.239999771118164, 11.239999771118164, 28] as Vector3; + for (const rotation of testRotations) { + // When using the rotation of the flycam for calculations, one has to invert the z value and interpret the resulting euler angle as ZYX. + // More info about this at https://www.notion.so/scalableminds/3D-Rotations-3D-Scene-210b51644c6380c2a4a6f5f3c069738a?source=copy_link#22bb51644c6380138fdac454d4dac2f0. + const rotationCorrected = [rotation[0], rotation[1], -rotation[2]] as Vector3; + const rotationInRadian = map3(THREE.MathUtils.degToRad, rotationCorrected); + for (const offset of testOffsets) { + const stateWithAnisotropicScale = update(initialState, { + dataset: { dataSource: { scale: { factor: { $set: anisotropicDatasetScale } } } }, + }); + const rotateAction = setRotationAction(rotation); + const rotatedState = FlycamReducer(stateWithAnisotropicScale, rotateAction); + const clickPositionAtViewportCenter = { + x: viewportSize / 2 + offset[0], + y: viewportSize / 2 + offset[1], + }; + const globalPosition = accessors.calculateGlobalPos( + rotatedState, + clickPositionAtViewportCenter, + ); + // Applying the rotation of 83° around the x axis to the offset and the apply the dataset scale. + const scaleFactor = getBaseVoxelFactorsInUnit(rotatedState.dataset.dataSource.scale); + const rotatedAndScaledOffset = new THREE.Vector3(offset[0], offset[1], 0) + .applyEuler(new THREE.Euler(...rotationInRadian, "ZYX")) + .multiply(new THREE.Vector3(...scaleFactor)) + .toArray(); + const expectedPosition = [ + ...V3.add(initialFlycamPosition, V3.round(rotatedAndScaledOffset)), + ] as Vector3; + console.error( + "rotationInRadian", + rotationInRadian, + "rotatedAndScaledOffset", + rotatedAndScaledOffset, + ); + almostEqual( + expect, + globalPosition.rounded, + expectedPosition, + 2, + `Global position is wrong with offset ${offset} and rotation ${rotation} in viewport ${OrthoViews.PLANE_XY}. Expected ${globalPosition.rounded} to nearly equal ${expectedPosition}.`, + ); + } + } + }); + it("should calculate the correct global position and correctly calculate it back to viewports perceptive position when flycam is rotated and with a dataset with an even scale.", () => { + const anisotropicDatasetScale = [1, 1, 1] as Vector3; + for (const rotation of testRotations) { + // When using the rotation of the flycam for calculations, one has to invert the z value and interpret the resulting euler angle as ZYX. + // More info about this at https://www.notion.so/scalableminds/3D-Rotations-3D-Scene-210b51644c6380c2a4a6f5f3c069738a?source=copy_link#22bb51644c6380138fdac454d4dac2f0. + const rotationCorrected = [rotation[0], rotation[1], -rotation[2]] as Vector3; + const rotationInRadian = map3(THREE.MathUtils.degToRad, rotationCorrected); + for (const offset of testOffsets) { + for (const planeId of OrthoViewValuesWithoutTDView) { + const stateWithAnisotropicScale = update(initialState, { + dataset: { dataSource: { scale: { factor: { $set: anisotropicDatasetScale } } } }, + viewModeData: { plane: { activeViewport: { $set: planeId } } }, + }); + const rotateAction = setRotationAction(rotation); + const rotatedState = FlycamReducer(stateWithAnisotropicScale, rotateAction); + const clickPositionAtViewportCenter = { + x: viewportSize / 2 + offset[0], + y: viewportSize / 2 + offset[1], + }; + const globalPosition = accessors.calculateGlobalPos( + rotatedState, + clickPositionAtViewportCenter, + ); + // Applying the rotation of 83° around the x axis to the offset and the apply the dataset scale. + const scaleFactor = getBaseVoxelFactorsInUnit(rotatedState.dataset.dataSource.scale); + const posInViewportVector = accessors.calculateInViewportPos( + globalPosition.floating, + initialFlycamPosition, + rotationInRadian, + scaleFactor, + rotatedState.flycam.zoomStep, + ); + const posInViewport = Dimensions.transDim( + [posInViewportVector.x, posInViewportVector.y, posInViewportVector.z], + planeId, + ); + const expected3DOffset = [...offset, 0] as Vector3; + almostEqual( + expect, + posInViewport, + expected3DOffset, + 2, + `Global position is wrong with offset ${offset} and rotation ${rotation} in viewport ${OrthoViews.PLANE_XY}. Expected ${posInViewport} to nearly equal ${expected3DOffset}.`, + ); + } + } + } + }); + it("should calculate the correct global position and correctly calculate it back to viewports perceptive position when flycam is rotated and with a dataset with an anisotropic scale.", () => { + const anisotropicDatasetScale = [11.239999771118164, 11.239999771118164, 28] as Vector3; + for (const rotation of testRotations) { + // When using the rotation of the flycam for calculations, one has to invert the z value and interpret the resulting euler angle as ZYX. + // More info about this at https://www.notion.so/scalableminds/3D-Rotations-3D-Scene-210b51644c6380c2a4a6f5f3c069738a?source=copy_link#22bb51644c6380138fdac454d4dac2f0. + const rotationCorrected = [rotation[0], rotation[1], -rotation[2]] as Vector3; + const rotationInRadian = map3(THREE.MathUtils.degToRad, rotationCorrected); + for (const offset of testOffsets) { + for (const planeId of OrthoViewValuesWithoutTDView) { + const stateWithAnisotropicScale = update(initialState, { + dataset: { dataSource: { scale: { factor: { $set: anisotropicDatasetScale } } } }, + viewModeData: { plane: { activeViewport: { $set: planeId } } }, + }); + const rotateAction = setRotationAction(rotation); + const rotatedState = FlycamReducer(stateWithAnisotropicScale, rotateAction); + const clickPositionAtViewportCenter = { + x: viewportSize / 2 + offset[0], + y: viewportSize / 2 + offset[1], + }; + const globalPosition = accessors.calculateGlobalPos( + rotatedState, + clickPositionAtViewportCenter, + ); + // Applying the rotation of 83° around the x axis to the offset and the apply the dataset scale. + const scaleFactor = getBaseVoxelFactorsInUnit(rotatedState.dataset.dataSource.scale); + const posInViewportVector = accessors.calculateInViewportPos( + globalPosition.floating, + initialFlycamPosition, + rotationInRadian, + scaleFactor, + rotatedState.flycam.zoomStep, + ); + const posInViewport = Dimensions.transDim( + [posInViewportVector.x, posInViewportVector.y, posInViewportVector.z], + planeId, + ); + const expected3DOffset = [...offset, 0] as Vector3; + almostEqual( + expect, + posInViewport, + expected3DOffset, + 2, + `Global position is wrong with offset ${offset} and rotation ${rotation} in viewport ${OrthoViews.PLANE_XY}. Expected ${posInViewport} to nearly equal ${expected3DOffset}.`, + ); + } + } + } + }); +}); From 3f415686fd88ac9522b9a01c981f843215965ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:38:57 +0200 Subject: [PATCH 120/128] fix imports --- .../test/model/accessors/view_mode_accessors.spec.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/javascripts/test/model/accessors/view_mode_accessors.spec.ts b/frontend/javascripts/test/model/accessors/view_mode_accessors.spec.ts index cfa15f3236f..0f5c983cc09 100644 --- a/frontend/javascripts/test/model/accessors/view_mode_accessors.spec.ts +++ b/frontend/javascripts/test/model/accessors/view_mode_accessors.spec.ts @@ -2,13 +2,7 @@ import update from "immutability-helper"; import { describe, it, expect } from "vitest"; import type { WebknossosState } from "viewer/store"; import * as accessors from "viewer/model/accessors/view_mode_accessor"; -import { - OrthoViews, - OrthoViewValuesWithoutTDView, - UnitLong, - type Vector2, - type Vector3, -} from "viewer/constants"; +import { OrthoViews, OrthoViewValuesWithoutTDView, UnitLong, type Vector3 } from "viewer/constants"; import defaultState from "viewer/default_state"; import { FlycamMatrixWithDefaultRotation } from "test/fixtures/flycam_object"; import { M4x4, V3 } from "libs/mjs"; From a1ea02c9e9d2ffead40358dd815a175564cce0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:40:47 +0200 Subject: [PATCH 121/128] fix proofreading test? --- frontend/javascripts/test/sagas/proofreading.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/javascripts/test/sagas/proofreading.spec.ts b/frontend/javascripts/test/sagas/proofreading.spec.ts index 93ed28f1740..db138e4078e 100644 --- a/frontend/javascripts/test/sagas/proofreading.spec.ts +++ b/frontend/javascripts/test/sagas/proofreading.spec.ts @@ -292,8 +292,6 @@ describe("Proofreading", () => { }, ]); - yield take("FINISH_MAPPING_INITIALIZATION"); - const mapping1 = yield select( (state) => getMappingInfo(state.temporaryConfiguration.activeMappingByLayer, tracingId).mapping, From c405b655d1dba74abb22814aa4d5b132c122dec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:08:30 +0200 Subject: [PATCH 122/128] edit comment explaining why the outer vertices of the vertex moving optimization code in the vertex shader are handled correctly --- .../viewer/shaders/main_data_shaders.glsl.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index d91bb3972d2..7125733c771 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -500,8 +500,8 @@ void main() { // of the currently rendered magnification. // In general, an index i is computed for each vertex so that each vertex can be moved // to the right/bottom border of the i-th bucket. - // Exceptions are the first and the last vertex which aren't moved so that the plane - // keeps its original extent. + // To ensure that the outer vertices are not moved to the next lower / higher bucket border + // the vertices are clamp to stay in range of worldCoordTopLeft and worldCoordBottomRight. // Calculate the index of the vertex (e.g., index.x=0 is the first horizontal vertex). // Let's only consider x: @@ -524,24 +524,24 @@ void main() { vec3 voxelSizeFactorInvertedUVW = transDim(voxelSizeFactorInverted); vec3 transWorldCoord = transDim(worldCoord.xyz); - transWorldCoord.x = - ( - // Left border of left-most bucket (probably outside of visible plane) - floor(worldCoordTopLeft.x * voxelSizeFactorInvertedUVW.x / d.x) * d.x - // Move by index.x buckets to the right. - + index.x * d.x - ) * voxelSizeFactorUVW.x; - - transWorldCoord.x = clamp(transWorldCoord.x, worldCoordTopLeft.x, worldCoordBottomRight.x); - - transWorldCoord.y = - ( - // Top border of top-most bucket (probably outside of visible plane) - floor(worldCoordTopLeft.y * voxelSizeFactorInvertedUVW.y / d.y) * d.y - // Move by index.y buckets to the bottom. - + index.y * d.y - ) * voxelSizeFactorUVW.y; - transWorldCoord.y = clamp(transWorldCoord.y, worldCoordTopLeft.y, worldCoordBottomRight.y); + transWorldCoord.x = + ( + // Left border of left-most bucket (probably outside of visible plane) + floor(worldCoordTopLeft.x * voxelSizeFactorInvertedUVW.x / d.x) * d.x + // Move by index.x buckets to the right. + + index.x * d.x + ) * voxelSizeFactorUVW.x; + + transWorldCoord.x = clamp(transWorldCoord.x, worldCoordTopLeft.x, worldCoordBottomRight.x); + + transWorldCoord.y = + ( + // Top border of top-most bucket (probably outside of visible plane) + floor(worldCoordTopLeft.y * voxelSizeFactorInvertedUVW.y / d.y) * d.y + // Move by index.y buckets to the bottom. + + index.y * d.y + ) * voxelSizeFactorUVW.y; + transWorldCoord.y = clamp(transWorldCoord.y, worldCoordTopLeft.y, worldCoordBottomRight.y); worldCoord = vec4(transDim(transWorldCoord), 1.); From 77c64c23636ceff43321e840f199af5b2f70254d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:11:21 +0200 Subject: [PATCH 123/128] add assertion to proofreading test that tool wasn't incorrectly switched --- frontend/javascripts/test/sagas/proofreading.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/javascripts/test/sagas/proofreading.spec.ts b/frontend/javascripts/test/sagas/proofreading.spec.ts index db138e4078e..09ba5b2b42c 100644 --- a/frontend/javascripts/test/sagas/proofreading.spec.ts +++ b/frontend/javascripts/test/sagas/proofreading.spec.ts @@ -302,6 +302,9 @@ describe("Proofreading", () => { yield call(() => api.tracing.save()); expect(context.receivedDataPerSaveRequest).toEqual([]); + + const activeTool = yield select((state) => state.uiInformation.activeTool); + expect(activeTool).toBe(AnnotationTool.PROOFREAD); }); await task.toPromise(); From 4121bc336f62c657f6058945109bb187881bd95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:38:34 +0200 Subject: [PATCH 124/128] add comment explaining default stored rotation of [0,0,180] --- frontend/javascripts/viewer/default_state.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/javascripts/viewer/default_state.ts b/frontend/javascripts/viewer/default_state.ts index 3ec7458b4b1..caf89b83baf 100644 --- a/frontend/javascripts/viewer/default_state.ts +++ b/frontend/javascripts/viewer/default_state.ts @@ -200,6 +200,9 @@ const defaultState: WebknossosState = { spaceDirectionOrtho: [1, 1, 1], direction: [0, 0, 0], additionalCoordinates: [], + // The flycam matrix has a default rotation of 180° around the z axis (see flycam_reducer.tsx resetMatrix) which is already + // calculated out of the rotation value shown to the user and stored in this property. But as the initial matrix above + // does not have this default rotation, the correct resulting rotation value matching the identity matrix is [0,0,180]. rotation: [0, 0, 180], }, flycamInfoCache: { From 01bbc9a65d25c81b15aaa330080e8f8ea6b32551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:52:21 +0200 Subject: [PATCH 125/128] fix zoom to position - done by introducing new action translating the flycam absolute without any rotation / zoom etc. interference --- .../test/reducers/flycam_reducer.spec.ts | 14 ++++++++++++++ .../controller/combinations/move_handlers.ts | 13 +++++-------- .../viewer/model/actions/flycam_actions.ts | 10 ++++++++++ .../viewer/model/reducers/flycam_reducer.ts | 17 ++++++++++++++++- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts index 0800e608aec..00ccc9890eb 100644 --- a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts @@ -80,6 +80,20 @@ describe("Flycam", () => { equalWithEpsilon(getPosition(newState.flycam), [-2, -4, 6]); }); + it("should move the flycam absolute without taking base rotation into account", () => { + const moveAction = FlycamActions.moveFlycamAbsoluteAction([1, 2, 3]); + const newState = FlycamReducer(initialState, moveAction); + // The absolute action should move the flycam without taking the rotation into account. So no invert. + equalWithEpsilon(getPosition(newState.flycam), [1, 2, 3]); + }); + + it("should move the flycam absolute backwards without taking base rotation into account", () => { + const moveAction = FlycamActions.moveFlycamAbsoluteAction([-1, -2, -3]); + const newState = FlycamReducer(initialState, moveAction); + // The absolute action should move the flycam without taking the rotation into account. So no invert. + equalWithEpsilon(getPosition(newState.flycam), [-1, -2, -3]); + }); + it("should set the rotation the flycam", () => { const rotateAction = FlycamActions.setRotationAction([180, 0, 0]); const newState = FlycamReducer(initialState, rotateAction); diff --git a/frontend/javascripts/viewer/controller/combinations/move_handlers.ts b/frontend/javascripts/viewer/controller/combinations/move_handlers.ts index 4354063434d..4bce751bfa8 100644 --- a/frontend/javascripts/viewer/controller/combinations/move_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/move_handlers.ts @@ -1,9 +1,11 @@ +import { V3 } from "libs/mjs"; import type { OrthoView, Point2, Vector3 } from "viewer/constants"; import { OrthoViewValuesWithoutTDView, OrthoViews } from "viewer/constants"; import { is2dDataset } from "viewer/model/accessors/dataset_accessor"; import { getActiveMagInfo } from "viewer/model/accessors/flycam_accessor"; import { calculateGlobalPos, getInputCatcherRect } from "viewer/model/accessors/view_mode_accessor"; import { + moveFlycamAbsoluteAction, moveFlycamOrthoAction, movePlaneFlycamOrthoAction, zoomByDeltaAction, @@ -93,7 +95,7 @@ function getMousePosition() { return calculateGlobalPos(state, { x: mousePosition[0], y: mousePosition[1], - }).rounded; + }).floating; } export function zoomPlanes(value: number, zoomToMouse: boolean): void { @@ -122,12 +124,7 @@ function finishZoom(oldMousePosition: Vector3): void { return; } - const moveVector = [ - oldMousePosition[0] - mousePos[0], - oldMousePosition[1] - mousePos[1], - oldMousePosition[2] - mousePos[2], - ]; - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'number[]' is not assignable to p... Remove this comment to see the full error message - Store.dispatch(moveFlycamOrthoAction(moveVector, activeViewport)); + const moveVector = V3.sub(oldMousePosition, mousePos); + Store.dispatch(moveFlycamAbsoluteAction(moveVector)); } } diff --git a/frontend/javascripts/viewer/model/actions/flycam_actions.ts b/frontend/javascripts/viewer/model/actions/flycam_actions.ts index 9d386806ab2..a48e0a0e9ff 100644 --- a/frontend/javascripts/viewer/model/actions/flycam_actions.ts +++ b/frontend/javascripts/viewer/model/actions/flycam_actions.ts @@ -12,6 +12,7 @@ type SetDirectionAction = ReturnType; type MoveFlycamOrthoAction = ReturnType; type MovePlaneFlycamOrthoAction = ReturnType; type MoveFlycamAction = ReturnType; +type MoveFlycamAbsoluteAction = ReturnType; type YawFlycamAction = ReturnType; type RollFlycamAction = ReturnType; type PitchFlycamAction = ReturnType; @@ -27,6 +28,7 @@ export type FlycamAction = | SetRotationAction | SetDirectionAction | MoveFlycamAction + | MoveFlycamAbsoluteAction | MoveFlycamOrthoAction | MovePlaneFlycamOrthoAction | YawFlycamAction @@ -45,6 +47,7 @@ export const FlycamActions = [ "MOVE_FLYCAM_ORTHO", "MOVE_PLANE_FLYCAM_ORTHO", "MOVE_FLYCAM", + "MOVE_FLYCAM_ABSOLUTE", "YAW_FLYCAM", "ROLL_FLYCAM", "PITCH_FLYCAM", @@ -123,6 +126,13 @@ export const moveFlycamAction = (vector: Vector3) => vector, }) as const; +// Use when a translation should be done directly to the position and rotation, zoom and more should be ignored. +export const moveFlycamAbsoluteAction = (vector: Vector3) => + ({ + type: "MOVE_FLYCAM_ABSOLUTE", + vector, + }) as const; + export const yawFlycamAction = (angle: number, regardDistance: boolean = false) => ({ type: "YAW_FLYCAM", diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 4f0bfe7140a..6ed6d5d0c4e 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -305,8 +305,15 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState // if the action vector is invalid, do not update return state; } - + console.log("translating by ", [...action.vector]); + console.log("prev pos", [ + state.flycam.currentMatrix[12], + state.flycam.currentMatrix[13], + state.flycam.currentMatrix[14], + ]); const newMatrix = M4x4.translate(action.vector, state.flycam.currentMatrix, []); + console.log("after pos", [newMatrix[12], newMatrix[13], newMatrix[14]]); + // TODO: Check whether the output is correct. Else maybe use the move reducer! return update(state, { flycam: { currentMatrix: { @@ -316,6 +323,14 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState }); } + case "MOVE_FLYCAM_ABSOLUTE": { + if (action.vector.includes(Number.NaN)) { + // if the action vector is invalid, do not update + return state; + } + return moveReducer(state, action.vector); + } + case "MOVE_FLYCAM_ORTHO": { const vector = _.clone(action.vector); From 704a111bdf4ff9a0a5b13c3113ba13b9a5b8149b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:18:21 +0200 Subject: [PATCH 126/128] remove debugging logging --- .../javascripts/viewer/model/reducers/flycam_reducer.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 6ed6d5d0c4e..627ab7aa19b 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -305,15 +305,7 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState // if the action vector is invalid, do not update return state; } - console.log("translating by ", [...action.vector]); - console.log("prev pos", [ - state.flycam.currentMatrix[12], - state.flycam.currentMatrix[13], - state.flycam.currentMatrix[14], - ]); const newMatrix = M4x4.translate(action.vector, state.flycam.currentMatrix, []); - console.log("after pos", [newMatrix[12], newMatrix[13], newMatrix[14]]); - // TODO: Check whether the output is correct. Else maybe use the move reducer! return update(state, { flycam: { currentMatrix: { From 5a7b341bea841164e638f632fee91fb1e473c935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:20:58 +0200 Subject: [PATCH 127/128] add new flycam action to ignored list --- .../javascripts/viewer/model/helpers/action_logger_middleware.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts b/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts index 188930c22dc..129ea49a6c3 100644 --- a/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts +++ b/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts @@ -13,6 +13,7 @@ let lastActionCount: number = 0; const actionBlacklist = [ "ADD_TO_LAYER", "MOVE_FLYCAM", + "MOVE_FLYCAM_ABSOLUTE", "MOVE_FLYCAM_ORTHO", "MOVE_PLANE_FLYCAM_ORTHO", "PUSH_SAVE_QUEUE_TRANSACTION", From 3ecab953af923ae46bd44f48bb64d1e49bcd331c Mon Sep 17 00:00:00 2001 From: MichaelBuessemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 15 Jul 2025 08:22:11 +0200 Subject: [PATCH 128/128] Update frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts Co-authored-by: Philipp Otto --- frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index 7125733c771..25865deb376 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -501,7 +501,7 @@ void main() { // In general, an index i is computed for each vertex so that each vertex can be moved // to the right/bottom border of the i-th bucket. // To ensure that the outer vertices are not moved to the next lower / higher bucket border - // the vertices are clamp to stay in range of worldCoordTopLeft and worldCoordBottomRight. + // the vertices are clamped to stay in range of worldCoordTopLeft and worldCoordBottomRight. // Calculate the index of the vertex (e.g., index.x=0 is the first horizontal vertex). // Let's only consider x: