diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index 6da099b3bf7..41f56e92664 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -1961,6 +1961,7 @@ export function computeAdHocMesh( cubeSize: V3.toArray(V3.add(cubeSize, [1, 1, 1])), //cubeSize is in target mag // Name and type of mapping to apply before building mesh (optional) mapping: mappingName, + // todop: is this supposed to be the original scale factor? voxelSizeFactorInUnit: scaleFactor, mag, ...rest, diff --git a/frontend/javascripts/libs/format_utils.ts b/frontend/javascripts/libs/format_utils.ts index eeb7c0908b6..774c24b0ad2 100644 --- a/frontend/javascripts/libs/format_utils.ts +++ b/frontend/javascripts/libs/format_utils.ts @@ -11,7 +11,13 @@ import utc from "dayjs/plugin/utc"; import weekday from "dayjs/plugin/weekday"; import * as Utils from "libs/utils"; import _ from "lodash"; -import { LongUnitToShortUnitMap, UnitShort, type Vector3, type Vector6 } from "viewer/constants"; +import { + LongUnitToShortUnitMap, + UnitShort, + type Vector3, + type Vector6, + ShortUnitToLongUnitMap, +} from "viewer/constants"; import { Unicode } from "viewer/constants"; import type { Duration } from "dayjs/plugin/duration"; @@ -143,6 +149,23 @@ export function formatScale(scale: VoxelSize | null | undefined, roundTo: number if (scale == null) { return ""; } + const optimizedScale = optimizeScaleUnitInVoxelSize(scale, roundTo); + const { factor, unit } = optimizedScale; + const scaleInNmRounded = Utils.map3((value) => Utils.roundTo(value, roundTo), factor); + return `${scaleInNmRounded.join(ThinSpace + MultiplicationSymbol + ThinSpace)} ${unit}³/voxel`; +} + +export function optimizeScaleUnitInVoxelSize( + scale: VoxelSize, + decimalPrecision: number = 1, +): VoxelSize { + /* + * Changes the unit in VoxelSize so that it is well suited to + * the values in the scale factor. + */ + if (scale == null) { + return scale; + } const scaleFactor = scale.factor; const smallestScaleFactor = Math.min(...scaleFactor); const unitDimension = { unit: LongUnitToShortUnitMap[scale.unit], dimension: 1 }; @@ -151,13 +174,13 @@ export function formatScale(scale: VoxelSize | null | undefined, roundTo: number unitDimension, nmFactorToUnit, false, - roundTo, - ); - const scaleInNmRounded = Utils.map3( - (value) => Utils.roundTo(value / conversionFactor, roundTo), - scaleFactor, + decimalPrecision, ); - return `${scaleInNmRounded.join(ThinSpace + MultiplicationSymbol + ThinSpace)} ${newUnit}³/voxel`; + const adaptedScale = Utils.map3((value) => value / conversionFactor, scaleFactor); + return { + factor: adaptedScale, + unit: ShortUnitToLongUnitMap[newUnit], + }; } function toOptionalFixed(num: number, decimalPrecision: number): string { @@ -193,24 +216,24 @@ function formatNumberInUnit( } export const nmFactorToUnit = new Map([ - [1e-15, "ym"], - [1e-12, "zm"], - [1e-9, "am"], - [1e-6, "fm"], - [1e-3, "pm"], - [1, "nm"], - [1e3, "µm"], - [1e6, "mm"], - [1e7, "cm"], - [1e9, "m"], - [1e12, "km"], - [1e15, "Mm"], - [1e18, "Gm"], - [1e21, "Tm"], - [1e24, "Pm"], - [1e27, "Em"], - [1e30, "Zm"], - [1e33, "Ym"], + [1e-15, UnitShort.ym], + [1e-12, UnitShort.zm], + [1e-9, UnitShort.am], + [1e-6, UnitShort.fm], + [1e-3, UnitShort.pm], + [1, UnitShort.nm], + [1e3, UnitShort.µm], + [1e6, UnitShort.mm], + [1e7, UnitShort.cm], + [1e9, UnitShort.m], + [1e12, UnitShort.km], + [1e15, UnitShort.Mm], + [1e18, UnitShort.Gm], + [1e21, UnitShort.Tm], + [1e24, UnitShort.Pm], + [1e27, UnitShort.Em], + [1e30, UnitShort.Zm], + [1e33, UnitShort.Ym], ]); // Accepts a length that is interpreted in the given unit and returns a string @@ -345,7 +368,7 @@ export function formatCountToDataAmountUnit( ); } -const getSortedFactorsAndUnits = _.memoize((unitMap: Map) => +const getSortedFactorsAndUnits = _.memoize((unitMap: Map) => Array.from(unitMap.entries()).sort((a, b) => a[0] - b[0]), ); @@ -356,13 +379,13 @@ function adjustUnitToDimension(unit: UnitShort | ByteUnit, dimension: number): s return dimension === 1 ? unit : dimension === 2 ? `${unit}²` : `${unit}³`; } -function findBestUnitForFormatting( +function findBestUnitForFormatting( number: number, { unit, dimension }: UnitDimension, - unitMap: Map, + unitMap: Map, preferShorterDecimals: boolean = false, decimalPrecision: number = 1, -): [number, string] { +): [number, T] { const isLengthUnit = unit in UnitsMap; let factorToNextSmallestCommonUnit = 1; if (isLengthUnit) { diff --git a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts index 7471c4bbe82..91f17a8c03d 100644 --- a/frontend/javascripts/test/reducers/flycam_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/flycam_reducer.spec.ts @@ -30,6 +30,9 @@ const initialState = { dataLayers: [{ name: "color", type: "color", additionalCoordinates: [] }], }, }, + datasetConfiguration: { + nativelyRenderedLayerName: null, + }, userConfiguration: { sphericalCapRadius: 100, dynamicSpaceDirection: true, diff --git a/frontend/javascripts/test/shaders/shader_syntax.spec.ts b/frontend/javascripts/test/shaders/shader_syntax.spec.ts index 6e1529dc5f7..0df9ed81ed7 100644 --- a/frontend/javascripts/test/shaders/shader_syntax.spec.ts +++ b/frontend/javascripts/test/shaders/shader_syntax.spec.ts @@ -45,9 +45,9 @@ describe("Shader syntax", () => { orderedColorLayerNames: ["color_layer_1", "color_layer_2"], segmentationLayerNames: [], magnificationsCount: mags.length, - voxelSizeFactor: [1, 1, 1], + // voxelSizeFactor: [1, 1, 1], isOrthogonal: true, - voxelSizeFactorInverted: [1, 1, 1], + // voxelSizeFactorInverted: [1, 1, 1], useInterpolation: false, tpsTransformPerLayer: {}, }); @@ -98,10 +98,10 @@ describe("Shader syntax", () => { orderedColorLayerNames: ["color_layer_1", "color_layer_2"], segmentationLayerNames: ["segmentationLayer"], magnificationsCount: mags.length, - voxelSizeFactor: [1, 1, 1], + // voxelSizeFactor: [1, 1, 1], isOrthogonal: true, useInterpolation: false, - voxelSizeFactorInverted: [1, 1, 1], + // voxelSizeFactorInverted: [1, 1, 1], tpsTransformPerLayer: {}, }); parser.parse(code); @@ -144,10 +144,10 @@ describe("Shader syntax", () => { orderedColorLayerNames: ["color_layer_1", "color_layer_2"], segmentationLayerNames: ["segmentationLayer"], magnificationsCount: mags.length, - voxelSizeFactor: [1, 1, 1], + // voxelSizeFactor: [1, 1, 1], isOrthogonal: true, useInterpolation: true, - voxelSizeFactorInverted: [1, 1, 1], + // voxelSizeFactorInverted: [1, 1, 1], tpsTransformPerLayer: {}, }); @@ -182,10 +182,10 @@ describe("Shader syntax", () => { orderedColorLayerNames: ["color_layer_1", "color_layer_2"], segmentationLayerNames: [], magnificationsCount: mags.length, - voxelSizeFactor: [1, 1, 1], + // voxelSizeFactor: [1, 1, 1], isOrthogonal: false, useInterpolation: false, - voxelSizeFactorInverted: [1, 1, 1], + // voxelSizeFactorInverted: [1, 1, 1], tpsTransformPerLayer: {}, }); parser.parse(code); @@ -228,10 +228,10 @@ describe("Shader syntax", () => { orderedColorLayerNames: ["color_layer_1", "color_layer_2"], segmentationLayerNames: ["segmentationLayer"], magnificationsCount: mags.length, - voxelSizeFactor: [1, 1, 1], + // voxelSizeFactor: [1, 1, 1], isOrthogonal: false, useInterpolation: true, - voxelSizeFactorInverted: [1, 1, 1], + // voxelSizeFactorInverted: [1, 1, 1], tpsTransformPerLayer: {}, }); parser.parse(code); @@ -265,10 +265,10 @@ describe("Shader syntax", () => { orderedColorLayerNames: ["color_layer_1", "color_layer_2"], segmentationLayerNames: [], magnificationsCount: mags.length, - voxelSizeFactor: [1, 1, 1], + // voxelSizeFactor: [1, 1, 1], isOrthogonal: true, useInterpolation: false, - voxelSizeFactorInverted: [1, 1, 1], + // voxelSizeFactorInverted: [1, 1, 1], tpsTransformPerLayer: {}, }); parser.parse(code); diff --git a/frontend/javascripts/viewer/api/api_latest.ts b/frontend/javascripts/viewer/api/api_latest.ts index f7707665031..6563f08d2d5 100644 --- a/frontend/javascripts/viewer/api/api_latest.ts +++ b/frontend/javascripts/viewer/api/api_latest.ts @@ -55,7 +55,10 @@ import { getMappingInfoOrNull, getVisibleSegmentationLayer, } from "viewer/model/accessors/dataset_accessor"; -import { flatToNestedMatrix } from "viewer/model/accessors/dataset_layer_transformation_accessor"; +import { + flatToNestedMatrix, + getTransformedVoxelSize, +} from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getActiveMagIndexForLayer, getAdditionalCoordinatesAsString, @@ -1260,7 +1263,10 @@ class TracingApi { throw new Error(`Tree with id ${treeId} not found.`); } - const voxelSizeFactor = state.dataset.dataSource.scale.factor; + const voxelSizeFactor = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ).factor; // Pre-allocate vectors let lengthInUnitAcc = 0; let lengthInVxAcc = 0; @@ -1323,7 +1329,11 @@ class TracingApi { throw new Error("The nodes are not within the same tree."); } - const voxelSizeFactor = Store.getState().dataset.dataSource.scale.factor; + const state = Store.getState(); + const voxelSizeFactor = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ).factor; // We use the Dijkstra algorithm to get the shortest path between the nodes. const distanceMap: Record = {}; // The distance map is also maintained in voxel space. This information is only @@ -1347,7 +1357,6 @@ class TracingApi { }); priorityQueue.queue([sourceNodeId, 0]); - const state = Store.getState(); const getPos = (node: Readonly) => getNodePosition(node, state); while (priorityQueue.length > 0) { diff --git a/frontend/javascripts/viewer/constants.ts b/frontend/javascripts/viewer/constants.ts index b0fff22c2d1..e5a5a83d2d1 100644 --- a/frontend/javascripts/viewer/constants.ts +++ b/frontend/javascripts/viewer/constants.ts @@ -1,3 +1,4 @@ +import _ from "lodash"; import { Euler, Matrix4 } from "three"; export type AdditionalCoordinate = { name: string; value: number }; @@ -495,6 +496,11 @@ export const LongUnitToShortUnitMap: Record = { [UnitLong.pc]: UnitShort.pc, }; +export const ShortUnitToLongUnitMap = _.invert(LongUnitToShortUnitMap) as Record< + UnitShort, + UnitLong +>; + export const AllUnits = Object.values(UnitLong); export enum AnnotationTypeFilterEnum { diff --git a/frontend/javascripts/viewer/controller/camera_controller.ts b/frontend/javascripts/viewer/controller/camera_controller.ts index 8bb97140014..0d71f3de5c7 100644 --- a/frontend/javascripts/viewer/controller/camera_controller.ts +++ b/frontend/javascripts/viewer/controller/camera_controller.ts @@ -17,6 +17,7 @@ import { OrthoViews, } from "viewer/constants"; import { getDatasetExtentInUnit } from "viewer/model/accessors/dataset_accessor"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getPosition, getRotationInRadian } from "viewer/model/accessors/flycam_accessor"; import { getInputCatcherAspectRatio, @@ -132,7 +133,11 @@ class CameraController extends React.PureComponent { updateCamViewport(inputCatcherRects?: OrthoViewRects): void { const state = Store.getState(); const { clippingDistance } = state.userConfiguration; - const scaleFactor = getBaseVoxelInUnit(state.dataset.dataSource.scale.factor); + const transformedScaleFactor = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ).factor; + const scaleFactor = getBaseVoxelInUnit(transformedScaleFactor); for (const planeId of OrthoViewValuesWithoutTDView) { const [width, height] = getPlaneExtentInVoxelFromStore( @@ -176,7 +181,10 @@ class CameraController extends React.PureComponent { const state = Store.getState(); const globalPosition = getPosition(state.flycam); // camera position's unit is nm, so convert it. - const cameraPosition = voxelToUnit(state.dataset.dataSource.scale, globalPosition); + const cameraPosition = voxelToUnit( + getTransformedVoxelSize(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName), + globalPosition, + ); // 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"); @@ -258,7 +266,10 @@ export function rotate3DViewTo( const state = Store.getState(); const { dataset } = state; const { tdCamera } = state.viewModeData.plane; - const flycamPos = voxelToUnit(dataset.dataSource.scale, getPosition(state.flycam)); + const flycamPos = voxelToUnit( + getTransformedVoxelSize(dataset, state.datasetConfiguration.nativelyRenderedLayerName), + getPosition(state.flycam), + ); const flycamRotation = getRotationInRadian(state.flycam); const datasetExtent = getDatasetExtentInUnit(dataset); // This distance ensures that the 3D camera is so far "in the back" that all elements in the scene diff --git a/frontend/javascripts/viewer/controller/combinations/bounding_box_handlers.ts b/frontend/javascripts/viewer/controller/combinations/bounding_box_handlers.ts index 05c35859aeb..231e5b873e3 100644 --- a/frontend/javascripts/viewer/controller/combinations/bounding_box_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/bounding_box_handlers.ts @@ -4,6 +4,7 @@ import _ from "lodash"; import type { BoundingBoxMinMaxType } from "types/bounding_box"; import type { OrthoView, Point2, Vector2, Vector3 } from "viewer/constants"; import getSceneController from "viewer/controller/scene_controller_provider"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getSomeTracing } from "viewer/model/accessors/tracing_accessor"; import { calculateGlobalDelta, @@ -169,7 +170,11 @@ export function getClosestHoveredBoundingBox( const { userBoundingBoxes } = getSomeTracing(state.annotation); const indices = Dimension.getIndices(plane); - const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const planeRatio = getBaseVoxelFactorsInUnit(transformedVoxelSize); const thirdDim = indices[2]; const zoomedMaxDistanceToSelection = MAX_DISTANCE_TO_SELECTION * state.flycam.zoomStep; let currentNearestDistance = zoomedMaxDistanceToSelection; diff --git a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts index 18847c9bed6..0ca8cc1b4ca 100644 --- a/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/skeleton_handlers.ts @@ -8,6 +8,7 @@ import { OrthoBaseRotations, OrthoViewToNumber, OrthoViews } from "viewer/consta import { getClosestHoveredBoundingBox } from "viewer/controller/combinations/bounding_box_handlers"; import getSceneController from "viewer/controller/scene_controller_provider"; import { getEnabledColorLayers } from "viewer/model/accessors/dataset_accessor"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getActiveMagIndicesForLayers, getFlycamRotationWithAppendedRotation, @@ -203,7 +204,11 @@ export function moveNode( const vectorRotated = movementVector.set(...vector).applyMatrix4(flycamRotationMatrix); const zoomFactor = state.flycam.zoomStep; - const scaleFactor = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const scaleFactor = getBaseVoxelFactorsInUnit(transformedVoxelSize); const op = (val: number) => { if (useFloat || isFlycamRotated) { diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index 3dff65a6d55..b28b22b1b92 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -53,6 +53,7 @@ import { getVisibleSegmentationLayers, } from "viewer/model/accessors/dataset_accessor"; import { + getTransformedVoxelSize, getTransformsForLayer, getTransformsForLayerOrNull, getTransformsForSkeletonLayer, @@ -92,7 +93,9 @@ class SceneController { private isPlaneVisible: OrthoViewMap; private clippingDistanceInUnit: number; private datasetBoundingBox!: Cube; + private planeGroup!: Group; private userBoundingBoxGroup!: Group; + private bucketDebbugingGroup!: Group; private layerBoundingBoxGroup!: Group; private userBoundingBoxes!: Array; private layerBoundingBoxes!: { [layerName: string]: Cube }; @@ -104,10 +107,12 @@ class SceneController { public lineMeasurementGeometry!: LineMeasurementGeometry; public areaMeasurementGeometry!: ContourGeometry; private planes!: OrthoViewWithoutTDMap; - private rootNode!: Group; + private rootNodeInScaledGroup!: Group; + private rootNodeInUnscaledGroup!: Group; public renderer!: WebGLRenderer; public scene!: Scene; - public rootGroup!: Group; + public scaledRootGroup!: Group; + public unscaledRootGroup!: Group; public segmentMeshController: SegmentMeshController; private storePropertyUnsubscribers: Array<() => void>; private splitBoundaryMesh: Mesh | null = null; @@ -136,13 +141,15 @@ class SceneController { this.bindToEvents(); this.scene = new Scene(); this.highlightedBBoxId = null; - this.rootGroup = new Group(); + this.scaledRootGroup = new Group(); + this.unscaledRootGroup = new Group(); this.scene.add( - this.rootGroup.add( - this.rootNode, + this.scaledRootGroup.add( + this.rootNodeInScaledGroup, this.segmentMeshController.meshesLayerLODRootGroup, this.segmentMeshController.lightsGroup, ), + this.unscaledRootGroup.add(this.rootNodeInUnscaledGroup), ); // 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 @@ -150,9 +157,12 @@ class SceneController { // For some reason, all objects have to be put into a group object. Changing // scene.scale does not have an effect. // The dimension(s) with the highest mag will not be distorted. - this.rootGroup.scale.copy( - new ThreeVector3(...Store.getState().dataset.dataSource.scale.factor), + const state = Store.getState(); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, ); + this.scaledRootGroup.scale.copy(new ThreeVector3(...transformedVoxelSize.factor)); this.setupDebuggingMethods(); } @@ -181,7 +191,7 @@ class SceneController { cube.position.x = position[0] + bucketSize[0] / 2; cube.position.y = position[1] + bucketSize[1] / 2; cube.position.z = position[2] + bucketSize[2] / 2; - this.rootNode.add(cube); + this.bucketDebbugingGroup.add(cube); return cube; }; @@ -199,7 +209,7 @@ class SceneController { cube.position.x = position[0] + cubeLength[0] / 2; cube.position.y = position[1] + cubeLength[1] / 2; cube.position.z = position[2] + cubeLength[2] / 2; - this.rootNode.add(cube); + this.bucketDebbugingGroup.add(cube); return cube; }; @@ -216,7 +226,7 @@ class SceneController { points.push(new ThreeVector3(...b)); const geometry = new BufferGeometry().setFromPoints(points); const line = new Line(geometry, material); - this.rootNode.add(line); + this.bucketDebbugingGroup.add(line); renderedLines.push(line); }; @@ -224,19 +234,20 @@ class SceneController { // @ts-ignore window.removeLines = () => { for (const line of renderedLines) { - this.rootNode.remove(line); + this.bucketDebbugingGroup.remove(line); } renderedLines = []; }; // @ts-ignore - window.removeBucketMesh = (mesh: LineSegments) => this.rootNode.remove(mesh); + window.removeBucketMesh = (mesh: LineSegments) => this.bucketDebbugingGroup.remove(mesh); } createMeshes(): void { this.userBoundingBoxes = []; this.userBoundingBoxGroup = new Group(); + this.bucketDebbugingGroup = new Group(); this.layerBoundingBoxGroup = new Group(); this.annotationToolsGeometryGroup = new Group(); const state = Store.getState(); @@ -264,20 +275,24 @@ 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 Group(); + this.planeGroup = new Group(); for (const plane of _.values(this.planes)) { - planeGroup.add(...plane.getMeshes()); + this.planeGroup.add(...plane.getMeshes()); } + // todop: remove comment // 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 ThreeVector3(1, 1, 1).divide( - new ThreeVector3(...Store.getState().dataset.dataSource.scale.factor), - ), - ); - - this.rootNode = new Group().add( + // const transformedVoxelSize = getTransformedVoxelSize( + // state.dataset, + // state.datasetConfiguration.nativelyRenderedLayerName, + // ); + // this.planeGroup.scale.copy( + // new ThreeVector3(1, 1, 1).divide(new ThreeVector3(...transformedVoxelSize.factor)), + // ); + + this.rootNodeInScaledGroup = new Group().add( this.userBoundingBoxGroup, + this.bucketDebbugingGroup, this.layerBoundingBoxGroup, this.annotationToolsGeometryGroup.add( ...this.contour.getMeshes(), @@ -286,8 +301,8 @@ class SceneController { ...this.areaMeasurementGeometry.getMeshes(), ), ...this.datasetBoundingBox.getMeshes(), - planeGroup, ); + this.rootNodeInUnscaledGroup = new Group().add(this.planeGroup); if (state.annotation.skeleton != null) { this.addSkeleton((_state) => getSkeletonTracing(_state.annotation), true); @@ -321,11 +336,11 @@ class SceneController { surfaceGroup.add(spline); } - this.rootGroup.add(surfaceGroup); + this.scaledRootGroup.add(surfaceGroup); this.splitBoundaryMesh = splitBoundaryMesh; return () => { - this.rootGroup.remove(surfaceGroup); + this.scaledRootGroup.remove(surfaceGroup); this.splitBoundaryMesh = null; }; } @@ -341,7 +356,7 @@ class SceneController { const skeleton = new Skeleton(skeletonTracingSelector, supportsPicking); const skeletonGroup = skeleton.getRootGroup(); this.skeletons[skeletonGroup.id] = skeleton; - this.rootNode.add(skeletonGroup); + this.rootNodeInScaledGroup.add(skeletonGroup); return skeletonGroup.id; } @@ -350,7 +365,7 @@ class SceneController { const skeletonGroup = skeleton.getRootGroup(); skeleton.destroy(); delete this.skeletons[skeletonId]; - this.rootNode.remove(skeletonGroup); + this.rootNodeInScaledGroup.remove(skeletonGroup); } updateTaskBoundingBoxes( @@ -368,7 +383,7 @@ class SceneController { for (const [tracingId, _boundingBox] of Object.entries(this.taskCubeByTracingId)) { let taskCube = this.taskCubeByTracingId[tracingId]; if (taskCube != null) { - taskCube.getMeshes().forEach((mesh) => this.rootNode.remove(mesh)); + taskCube.getMeshes().forEach((mesh) => this.rootNodeInScaledGroup.remove(mesh)); } this.taskCubeByTracingId[tracingId] = null; } @@ -386,7 +401,7 @@ class SceneController { showCrossSections: true, isHighlighted: false, }); - taskCube.getMeshes().forEach((mesh) => this.rootNode.add(mesh)); + taskCube.getMeshes().forEach((mesh) => this.rootNodeInScaledGroup.add(mesh)); if (constants.MODES_ARBITRARY.includes(viewMode)) { taskCube?.setVisibility(false); @@ -530,10 +545,6 @@ class SceneController { app.vent.emit("rerender"); } - getRootNode(): Object3D { - return this.rootNode; - } - setUserBoundingBoxes(bboxes: Array): void { const newUserBoundingBoxGroup = new Group(); this.userBoundingBoxes = bboxes.map(({ boundingBox, isVisible, color, id }) => { @@ -551,9 +562,9 @@ class SceneController { bbCube.getMeshes().forEach((mesh) => newUserBoundingBoxGroup.add(mesh)); return bbCube; }); - this.rootNode.remove(this.userBoundingBoxGroup); + this.rootNodeInScaledGroup.remove(this.userBoundingBoxGroup); this.userBoundingBoxGroup = newUserBoundingBoxGroup; - this.rootNode.add(this.userBoundingBoxGroup); + this.rootNodeInScaledGroup.add(this.userBoundingBoxGroup); } private applyTransformToGroup(transform: Transform, group: Group | CustomLOD) { @@ -588,6 +599,14 @@ class SceneController { state.datasetConfiguration.nativelyRenderedLayerName, ); this.applyTransformToGroup(transformForBBoxes, this.userBoundingBoxGroup); + + const skeletonTransforms = getTransformsForSkeletonLayer( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + + this.applyTransformToGroup(skeletonTransforms, this.bucketDebbugingGroup); + const visibleSegmentationLayers = getVisibleSegmentationLayers(state); if (visibleSegmentationLayers.length === 0) { return; @@ -653,9 +672,21 @@ class SceneController { return [layer.name, bbCube]; }), ); - this.rootNode.remove(this.layerBoundingBoxGroup); + this.rootNodeInScaledGroup.remove(this.layerBoundingBoxGroup); this.layerBoundingBoxGroup = newLayerBoundingBoxGroup; - this.rootNode.add(this.layerBoundingBoxGroup); + this.rootNodeInScaledGroup.add(this.layerBoundingBoxGroup); + + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const transformedScale = new ThreeVector3(...transformedVoxelSize.factor); + + this.scaledRootGroup.scale.copy(transformedScale); + // todop + // this.planeGroup.scale.copy( + // new ThreeVector3(1, 1, 1).divide(new ThreeVector3(...transformedVoxelSize.factor)), + // ); } highlightUserBoundingBox(bboxId: number | null | undefined): void { @@ -747,7 +778,7 @@ class SceneController { plane.destroy(); } - this.rootNode = new Group(); + this.rootNodeInScaledGroup = new Group(); } bindToEvents(): void { diff --git a/frontend/javascripts/viewer/controller/segment_mesh_controller.ts b/frontend/javascripts/viewer/controller/segment_mesh_controller.ts index 8bbf6b2b9ad..b84d8592b5c 100644 --- a/frontend/javascripts/viewer/controller/segment_mesh_controller.ts +++ b/frontend/javascripts/viewer/controller/segment_mesh_controller.ts @@ -238,6 +238,12 @@ export default class SegmentMeshController { layerLODGroup.addLODMesh(targetGroup, lod); } targetGroup.segmentId = segmentId; + // const state = Store.getState(); + // const transformedVoxelSize = getTransformedVoxelSize( + // state.dataset, + // state.datasetConfiguration.nativelyRenderedLayerName, + // ); + // const dsScaleFactor = transformedVoxelSize.factor; const dsScaleFactor = Store.getState().dataset.dataSource.scale.factor; // If the mesh was calculated on a different magnification level, // the backend sends the scale factor of this magnification. @@ -251,6 +257,7 @@ export default class SegmentMeshController { scale[1] / dsScaleFactor[1], scale[2] / dsScaleFactor[2], ]; + // todop? does this need to be dynamic? targetGroup.scale.copy(new ThreeVector3(...adaptedScale)); } const meshChunk = this.constructMesh(segmentId, layerName, geometry, opacity, isMerged); diff --git a/frontend/javascripts/viewer/controller/td_controller.tsx b/frontend/javascripts/viewer/controller/td_controller.tsx index c683c2300d8..e434d62556b 100644 --- a/frontend/javascripts/viewer/controller/td_controller.tsx +++ b/frontend/javascripts/viewer/controller/td_controller.tsx @@ -20,6 +20,7 @@ import { ProofreadToolController, SkeletonToolController, } from "viewer/controller/combinations/tool_controls"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getPosition } from "viewer/model/accessors/flycam_accessor"; import { getActiveNode, getNodePosition } from "viewer/model/accessors/skeletontracing_accessor"; import { AnnotationTool } from "viewer/model/accessors/tool_accessor"; @@ -90,7 +91,7 @@ type OwnProps = { annotation?: StoreAnnotation; }; type StateProps = { - voxelSize: VoxelSize; + transformedVoxelSize: VoxelSize; activeTool: AnnotationTool; }; type Props = OwnProps & StateProps; @@ -108,8 +109,8 @@ class TDController extends React.PureComponent { isStarted: boolean = false; componentDidMount() { - const { dataset, flycam } = Store.getState(); - this.oldUnitPos = voxelToUnit(dataset.dataSource.scale, getPosition(flycam)); + const { flycam } = Store.getState(); + this.oldUnitPos = voxelToUnit(this.props.transformedVoxelSize, getPosition(flycam)); this.isStarted = true; this.initMouse(); } @@ -159,7 +160,7 @@ class TDController extends React.PureComponent { initTrackballControls(view: HTMLElement): void { const { flycam } = Store.getState(); - const pos = voxelToUnit(this.props.voxelSize, getPosition(flycam)); + const pos = voxelToUnit(this.props.transformedVoxelSize, getPosition(flycam)); const tdCamera = this.props.cameras[OrthoViews.TDView]; this.controls = new TrackballControls( tdCamera, @@ -239,7 +240,7 @@ class TDController extends React.PureComponent { } const { hitPosition } = intersection; - const unscaledPosition = V3.divide3(hitPosition, this.props.voxelSize.factor); + const unscaledPosition = V3.divide3(hitPosition, this.props.transformedVoxelSize.factor); const state = Store.getState(); const isMultiCutToolActive = state.userConfiguration.isMultiSplitActive; @@ -326,7 +327,7 @@ class TDController extends React.PureComponent { const { flycam } = Store.getState(); const { controls } = this; position = position || getPosition(flycam); - const nmPosition = voxelToUnit(this.props.voxelSize, position); + const nmPosition = voxelToUnit(this.props.transformedVoxelSize, position); if (controls != null) { controls.target.set(...nmPosition); @@ -398,7 +399,10 @@ class TDController extends React.PureComponent { export function mapStateToProps(state: WebknossosState): StateProps { return { - voxelSize: state.dataset.dataSource.scale, + transformedVoxelSize: getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ), activeTool: state.uiInformation.activeTool, }; } diff --git a/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx b/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx index ebf5c3a2b7c..8154fd4bb42 100644 --- a/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx +++ b/frontend/javascripts/viewer/controller/viewmodes/plane_controller.tsx @@ -28,6 +28,7 @@ import getSceneController, { getSceneControllerOrNull, } from "viewer/controller/scene_controller_provider"; import TDController from "viewer/controller/td_controller"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getActiveMagIndexForLayer, getMoveOffset, @@ -270,10 +271,13 @@ function createDelayAwareMoveHandler( // Nothing should happen then, anyway. return 0; } - + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); const thirdDim = dimensions.thirdDimensionForPlane(activeViewport); const voxelPerSecond = - state.userConfiguration.moveValue / state.dataset.dataSource.scale.factor[thirdDim]; + state.userConfiguration.moveValue / transformedVoxelSize.factor[thirdDim]; if (state.userConfiguration.dynamicSpaceDirection && useDynamicSpaceDirection) { // Change direction of the value connected to space, based on the last direction diff --git a/frontend/javascripts/viewer/geometries/helper_geometries.ts b/frontend/javascripts/viewer/geometries/helper_geometries.ts index 40dd3a2b0d1..b591d6bb426 100644 --- a/frontend/javascripts/viewer/geometries/helper_geometries.ts +++ b/frontend/javascripts/viewer/geometries/helper_geometries.ts @@ -21,6 +21,7 @@ import { Vector2, } from "three"; import { type OrthoView, OrthoViews, type Vector3 } from "viewer/constants"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import Dimensions from "viewer/model/dimensions"; import { getBaseVoxelInUnit } from "viewer/model/scaleinfo"; import Store from "viewer/store"; @@ -197,7 +198,12 @@ export class QuickSelectGeometry { }); this.rectangle = new Mesh(geometry, material); - const baseWidth = getBaseVoxelInUnit(Store.getState().dataset.dataSource.scale.factor); + const state = Store.getState(); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const baseWidth = getBaseVoxelInUnit(transformedVoxelSize.factor); const centerGeometry = new PlaneGeometry(baseWidth, baseWidth); const centerMaterial = new MeshBasicMaterial({ color: this.centerMarkerColor, @@ -240,7 +246,12 @@ export class QuickSelectGeometry { rotateToViewport() { const { activeViewport } = Store.getState().viewModeData.plane; - const { factor: scaleFactor } = Store.getState().dataset.dataSource.scale; + const state = Store.getState(); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const scaleFactor = transformedVoxelSize.factor; const rotation = rotations[activeViewport]; if (!rotation) { return; diff --git a/frontend/javascripts/viewer/geometries/materials/node_shader.ts b/frontend/javascripts/viewer/geometries/materials/node_shader.ts index d0611102a04..f9241e6133a 100644 --- a/frontend/javascripts/viewer/geometries/materials/node_shader.ts +++ b/frontend/javascripts/viewer/geometries/materials/node_shader.ts @@ -4,6 +4,7 @@ import _ from "lodash"; import { type DataTexture, GLSL3, RawShaderMaterial } from "three"; import { ViewModeValues, ViewModeValuesIndices } from "viewer/constants"; import type { Uniforms } from "viewer/geometries/materials/plane_material_factory"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getTransformsForSkeletonLayer } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getZoomValue } from "viewer/model/accessors/flycam_accessor"; import { listenToStoreProperty } from "viewer/model/helpers/listener_helpers"; @@ -46,6 +47,10 @@ class NodeShader { setupUniforms(treeColorTexture: DataTexture): void { const state = Store.getState(); const { additionalCoordinates } = state.flycam; + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); this.uniforms = { planeZoomFactor: { // The flycam zoom is typically decomposed into an x- and y-factor @@ -55,7 +60,7 @@ class NodeShader { value: getZoomValue(state.flycam), }, voxelSizeMin: { - value: getBaseVoxelInUnit(state.dataset.dataSource.scale.factor), + value: getBaseVoxelInUnit(transformedVoxelSize.factor), }, overrideParticleSize: { value: state.userConfiguration.particleSize, diff --git a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts index c2804e7fcaf..aa69219dab9 100644 --- a/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts +++ b/frontend/javascripts/viewer/geometries/materials/plane_material_factory.ts @@ -22,6 +22,7 @@ import { getVisibleSegmentationLayer, } from "viewer/model/accessors/dataset_accessor"; import { + getTransformedVoxelSize, getTransformsForLayer, getTransformsPerLayer, invertAndTranspose, @@ -249,6 +250,8 @@ class PlaneMaterialFactory { isFlycamRotated: { value: false }, doAllLayersHaveTransforms: { value: false }, inverseFlycamRotationMatrix: { value: new Matrix4() }, + voxelSizeFactor: { value: [1, 1, 1] }, + voxelSizeFactorInverted: { value: [1, 1, 1] }, }; const activeMagIndices = getActiveMagIndicesForLayers(Store.getState()); @@ -600,6 +603,19 @@ class PlaneMaterialFactory { this.uniforms.inverseFlycamRotationMatrix.value = inverseFlycamRotationMatrix; }, ), + listenToStoreProperty( + (storeState) => + getTransformedVoxelSize( + storeState.dataset, + storeState.datasetConfiguration.nativelyRenderedLayerName, + ).factor, + (transformedScale) => { + this.uniforms.voxelSizeFactor.value = transformedScale; + + const voxelSizeFactorInverted = V3.divide3([1, 1, 1], transformedScale); + this.uniforms.voxelSizeFactorInverted.value = voxelSizeFactorInverted; + }, + ), ); const oldVisibilityPerLayer: Record = {}; this.storePropertyUnsubscribers.push( @@ -1105,9 +1121,9 @@ class PlaneMaterialFactory { ); const textureLayerInfos = getTextureLayerInfos(); - const { dataset } = state; - const voxelSizeFactor = dataset.dataSource.scale.factor; - const voxelSizeFactorInverted = V3.divide3([1, 1, 1], voxelSizeFactor); + // const { dataset } = state; + // const voxelSizeFactor = dataset.dataSource.scale.factor; + // const voxelSizeFactorInverted = V3.divide3([1, 1, 1], voxelSizeFactor); const { interpolation } = state.datasetConfiguration; const code = getMainFragmentShader({ globalLayerCount, @@ -1116,8 +1132,8 @@ class PlaneMaterialFactory { segmentationLayerNames, textureLayerInfos, magnificationsCount: this.getTotalMagCount(), - voxelSizeFactor, - voxelSizeFactorInverted, + // voxelSizeFactor, + // voxelSizeFactorInverted, isOrthogonal: this.isOrthogonal, useInterpolation: interpolation, tpsTransformPerLayer: this.scaledTpsInvPerLayer, @@ -1144,9 +1160,8 @@ class PlaneMaterialFactory { this.getLayersToRender(maximumLayerCountToRender); const textureLayerInfos = getTextureLayerInfos(); - const { dataset } = state; - const voxelSizeFactor = dataset.dataSource.scale.factor; - const voxelSizeFactorInverted = V3.divide3([1, 1, 1], voxelSizeFactor); + // const voxelSizeFactor = dataset.dataSource.scale.factor; + // const voxelSizeFactorInverted = V3.divide3([1, 1, 1], voxelSizeFactor); const { interpolation } = state.datasetConfiguration; return getMainVertexShader({ @@ -1156,8 +1171,8 @@ class PlaneMaterialFactory { segmentationLayerNames, textureLayerInfos, magnificationsCount: this.getTotalMagCount(), - voxelSizeFactor, - voxelSizeFactorInverted, + // voxelSizeFactor, + // voxelSizeFactorInverted, isOrthogonal: this.isOrthogonal, useInterpolation: interpolation, tpsTransformPerLayer: this.scaledTpsInvPerLayer, diff --git a/frontend/javascripts/viewer/geometries/plane.ts b/frontend/javascripts/viewer/geometries/plane.ts index efe349f1cf8..18030c103ce 100644 --- a/frontend/javascripts/viewer/geometries/plane.ts +++ b/frontend/javascripts/viewer/geometries/plane.ts @@ -23,6 +23,11 @@ import constants, { import PlaneMaterialFactory, { type PlaneShaderMaterial, } from "viewer/geometries/materials/plane_material_factory"; +import { + extractScaleFromTransformation, + getTransformedVoxelSize, + getTransformsForSkeletonLayer, +} from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { listenToStoreProperty } from "viewer/model/helpers/listener_helpers"; // A subdivision of 100 means that there will be 100 segments per axis @@ -53,12 +58,16 @@ class Plane { 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: Line; + // lastScaleFactors is set with the return value from getPlaneScalingFactor which depends on the size of the + // viewport and the zoom value. lastScaleFactors: [number, number]; // baseRotation is the base rotation the plane has in an unrotated scene. It will be applied additional to the flycams rotation. // Different baseRotations for each of the planes ensures that the planes stay orthogonal to each other. baseRotation: Euler; storePropertyUnsubscribers: Array<() => void> = []; - datasetScaleFactor: Vector3 = [1, 1, 1]; + originalDatasetScaleFactor: Vector3 = [1, 1, 1]; + transformedDatasetScaleFactor: Vector3 = [1, 1, 1]; + transformScale: Vector3 = [1, 1, 1]; // Properties are only created here to avoid new creating objects for each setRotation call. baseRotationMatrix = new Matrix4(); @@ -163,8 +172,18 @@ 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: Vector3 = V3.multiply([xFactor, yFactor, 1], this.datasetScaleFactor); + const scaleVector: Vector3 = V3.multiply( + [1 * xFactor, 1 * yFactor, 1], + V3.multiply(this.originalDatasetScaleFactor, this.transformScale), + // this.transformedDatasetScaleFactor, + ); + console.log("scaleVector", scaleVector); + + console.log("this.originalDatasetScaleFactor", this.originalDatasetScaleFactor); + console.log("this.transformedDatasetScaleFactor", this.transformedDatasetScaleFactor); + this.getMeshes().map((mesh) => mesh.scale.set(...scaleVector)); } @@ -189,10 +208,9 @@ class Plane { positionOffset: Vector3 = DEFAULT_POSITION_OFFSET, ): void => { // 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); + const scaledPosition = V3.multiply(originalPosition, this.transformedDatasetScaleFactor); // The offset is in world space already so no scaling is necessary. const offsetPosition = V3.add(scaledPosition, positionOffset); this.TDViewBorders.position.set(...offsetPosition); @@ -223,8 +241,49 @@ class Plane { bindToEvents(): void { this.storePropertyUnsubscribers = [ listenToStoreProperty( - (storeState) => storeState.dataset.dataSource.scale.factor, - (scaleFactor) => (this.datasetScaleFactor = scaleFactor), + (storeState) => + getTransformedVoxelSize( + storeState.dataset, + storeState.datasetConfiguration.nativelyRenderedLayerName, + ).factor, + (scaleFactor) => { + // todop / workaround so that setScale uses new factor + this.lastScaleFactors[0] = -1; + this.transformedDatasetScaleFactor = scaleFactor; + }, + true, + ), + listenToStoreProperty( + (storeState) => + getTransformedVoxelSize( + storeState.dataset, + storeState.datasetConfiguration.nativelyRenderedLayerName, + true, + ).factor, + (scaleFactor) => { + // todop / workaround so that setScale uses new factor + this.lastScaleFactors[0] = -1; + this.originalDatasetScaleFactor = scaleFactor; + }, + true, + ), + + listenToStoreProperty( + (storeState) => { + const transforms = getTransformsForSkeletonLayer( + storeState.dataset, + storeState.datasetConfiguration.nativelyRenderedLayerName, + ); + + return extractScaleFromTransformation(transforms); + }, + (transformScale) => { + // todop / workaround so that setScale uses new factor + this.lastScaleFactors[0] = -1; + // todop + this.transformScale = transformScale; + }, + true, ), ]; } diff --git a/frontend/javascripts/viewer/model/accessors/dataset_accessor.ts b/frontend/javascripts/viewer/model/accessors/dataset_accessor.ts index 52d6d36eca7..e10edaaad8f 100644 --- a/frontend/javascripts/viewer/model/accessors/dataset_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/dataset_accessor.ts @@ -275,18 +275,19 @@ export function getDatasetExtentInVoxel(dataset: APIDataset) { } export function getDatasetExtentInUnit(dataset: APIDataset): BoundingBoxObject { const extentInVoxel = getDatasetExtentInVoxel(dataset); - const scaleFactor = dataset.dataSource.scale.factor; + const unscaledScaleFactor = dataset.dataSource.scale.factor; const topLeft = extentInVoxel.topLeft.map( - (val, index) => val * scaleFactor[index], + (val, index) => val * unscaledScaleFactor[index], ) as any as Vector3; const extent = { topLeft, - width: extentInVoxel.width * scaleFactor[0], - height: extentInVoxel.height * scaleFactor[1], - depth: extentInVoxel.depth * scaleFactor[2], + width: extentInVoxel.width * unscaledScaleFactor[0], + height: extentInVoxel.height * unscaledScaleFactor[1], + depth: extentInVoxel.depth * unscaledScaleFactor[2], }; return extent; } + export function getDatasetExtentAsString( dataset: APIMaybeUnimportedDataset, inVoxel: boolean = true, diff --git a/frontend/javascripts/viewer/model/accessors/dataset_layer_transformation_accessor.ts b/frontend/javascripts/viewer/model/accessors/dataset_layer_transformation_accessor.ts index f4f9ab0fc5a..f1b8f5b3f70 100644 --- a/frontend/javascripts/viewer/model/accessors/dataset_layer_transformation_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/dataset_layer_transformation_accessor.ts @@ -10,6 +10,7 @@ import type { APISkeletonLayer, AffineTransformation, CoordinateTransformation, + VoxelSize, } from "types/api_types"; import { Identity4x4, @@ -30,6 +31,7 @@ import { transformPointUnscaled, } from "../helpers/transformation_helpers"; import { getLayerByName } from "./dataset_accessor"; +import { optimizeScaleUnitInVoxelSize } from "libs/format_utils"; const IDENTITY_MATRIX = [ [1, 0, 0, 0], @@ -177,11 +179,8 @@ function _getOriginalTransformsForLayerOrNull( if (!coordinateTransformations || coordinateTransformations.length === 0) { return null; } - - return combineCoordinateTransformations( - coordinateTransformations, - dataset.dataSource.scale.factor, - ); + const untransformedVoxelSize = dataset.dataSource.scale; + return combineCoordinateTransformations(coordinateTransformations, untransformedVoxelSize.factor); } export const getOriginalTransformsForLayerOrNull = memoizeWithTwoKeys( @@ -297,11 +296,12 @@ function _getTransformsForLayerThatDoesNotSupportTransformationConfigOrNull( ? getTransformsForLayerOrNull(dataset, usableReferenceLayer, nativelyRenderedLayerName) : null; return toIdentityTransformMaybe(someLayersTransformsMaybe); - } else if (nativelyRenderedLayerName != null && allLayersSameRotation) { + } else if (allLayersSameRotation) { // If all layers have the same transformations and at least one is rendered natively, this means that all layer should be rendered natively. return null; } + // nativelyRenderedLayerName is not null and the layers don't have a common rotation: // Compute the inverse of the layer that should be rendered natively. const nativeLayer = getLayerByName(dataset, nativelyRenderedLayerName, true); const transformsOfNativeLayer = getOriginalTransformsForLayerOrNull(dataset, nativeLayer); @@ -405,6 +405,15 @@ function isOnlyRotatedOrMirrored(transformation?: AffineTransformation) { ); } +export function extractScaleFromTransformation(transformation?: Transform): Vector3 { + if (!transformation) { + return [1, 1, 1]; + } + const threeMatrix = new Matrix4().fromArray(transformation.affineMatrix).transpose(); + threeMatrix.decompose(translation, quaternion, scale); + return [Math.abs(scale.x), Math.abs(scale.y), Math.abs(scale.z)]; +} + function hasValidTransformationCount(dataLayers: Array): boolean { return dataLayers.every((layer) => layer.coordinateTransformations?.length === 5); } @@ -535,3 +544,37 @@ export function layerToGlobalTransformedPosition( } return layerPos; } + +function _getTransformedVoxelSize( + dataset: APIDataset, + nativelyRenderedLayerName: string | null, + ignoreTransformation: boolean = false, +): VoxelSize { + const { scale } = dataset.dataSource; + if (ignoreTransformation) { + return scale; + } + const scaleFactor = scale.factor; + const transforms = getTransformsForSkeletonLayer(dataset, nativelyRenderedLayerName); + const transformFn = transformPointUnscaled(transforms); + + // Transform (0,0,0) and (scaleFactor) to compute effective scale ratio: + const base = transformFn([0, 0, 0]); + const scaled = transformFn(scaleFactor); + + // Compute the resulting scale difference + const transformedScale: Vector3 = [ + Math.abs(scaled[0] - base[0]), + Math.abs(scaled[1] - base[1]), + Math.abs(scaled[2] - base[2]), + ]; + const transformedVoxelSize = { + factor: transformedScale, + unit: scale.unit, + }; + + const optimizedScale = optimizeScaleUnitInVoxelSize(transformedVoxelSize); + console.log("optimizedScale", optimizedScale); + return transformedVoxelSize; +} +export const getTransformedVoxelSize = memoizeOne(_getTransformedVoxelSize); diff --git a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts index bfc3bf950da..d9e88849364 100644 --- a/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/flycam_accessor.ts @@ -46,6 +46,7 @@ import { } from "../helpers/transformation_helpers"; import { getMatrixScale, rotateOnAxis } from "../reducers/flycam_reducer"; import { reuseInstanceOnEquality } from "./accessor_helpers"; +import { getTransformedVoxelSize } from "./dataset_layer_transformation_accessor"; export const ZOOM_STEP_INTERVAL = 1.1; @@ -197,22 +198,30 @@ export function _getMaximumZoomForAllMags( } // Only exported for testing. -export const _getDummyFlycamMatrix = memoizeOne((scale: Vector3) => { - const scaleMatrix = getMatrixScale(scale); +export const _getDummyFlycamMatrix = memoizeOne((voxelSize: Vector3) => { + const scaleMatrix = getMatrixScale(voxelSize); return rotateOnAxis(M4x4.scale(scaleMatrix, M4x4.identity(), []), Math.PI, [0, 0, 1]); }); export function getMoveOffset(state: WebknossosState, timeFactor: number) { + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); return ( (state.userConfiguration.moveValue * timeFactor) / - getBaseVoxelInUnit(state.dataset.dataSource.scale.factor) / + getBaseVoxelInUnit(transformedVoxelSize.factor) / constants.FPS ); } export function getMoveOffset3d(state: WebknossosState, timeFactor: number) { + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); const { moveValue3d } = state.userConfiguration; - const baseVoxel = getBaseVoxelInUnit(state.dataset.dataSource.scale.factor); + const baseVoxel = getBaseVoxelInUnit(transformedVoxelSize.factor); return (moveValue3d * timeFactor) / baseVoxel / constants.FPS; } @@ -577,7 +586,7 @@ function getArea( rects: OrthoViewRects, position: Vector3, zoomStep: number, - voxelSize: VoxelSize, + maybeTransformedVoxelSize: VoxelSize, planeId: OrthoView, ): Area { const [u, v] = Dimensions.getIndices(planeId); @@ -586,7 +595,7 @@ function getArea( zoomStep, planeId, ).map((el) => el / 2); - const baseVoxelFactors = scaleInfo.getBaseVoxelFactorsInUnit(voxelSize); + const baseVoxelFactors = scaleInfo.getBaseVoxelFactorsInUnit(maybeTransformedVoxelSize); const uHalf = viewportWidthHalf * baseVoxelFactors[u]; const vHalf = viewportHeightHalf * baseVoxelFactors[v]; const isVisible = uHalf > 0 && vHalf > 0; @@ -607,13 +616,31 @@ function getAreas( rects: OrthoViewRects, position: Vector3, zoomStep: number, - voxelSize: VoxelSize, + transformedVoxelSize: VoxelSize, ): OrthoViewMap { // @ts-expect-error ts-migrate(2741) FIXME: Property 'TDView' is missing in type '{ PLANE_XY: ... Remove this comment to see the full error message return { - [OrthoViews.PLANE_XY]: getArea(rects, position, zoomStep, voxelSize, OrthoViews.PLANE_XY), - [OrthoViews.PLANE_XZ]: getArea(rects, position, zoomStep, voxelSize, OrthoViews.PLANE_XZ), - [OrthoViews.PLANE_YZ]: getArea(rects, position, zoomStep, voxelSize, OrthoViews.PLANE_YZ), + [OrthoViews.PLANE_XY]: getArea( + rects, + position, + zoomStep, + transformedVoxelSize, + OrthoViews.PLANE_XY, + ), + [OrthoViews.PLANE_XZ]: getArea( + rects, + position, + zoomStep, + transformedVoxelSize, + OrthoViews.PLANE_XZ, + ), + [OrthoViews.PLANE_YZ]: getArea( + rects, + position, + zoomStep, + transformedVoxelSize, + OrthoViews.PLANE_YZ, + ), }; } @@ -621,8 +648,11 @@ export function getAreasFromState(state: WebknossosState): OrthoViewMap { const position = getPosition(state.flycam); const rects = getViewportRects(state); const { zoomStep } = state.flycam; - const voxelSize = state.dataset.dataSource.scale; - return getAreas(rects, position, zoomStep, voxelSize); + const maybeTransformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + return getAreas(rects, position, zoomStep, maybeTransformedVoxelSize); } type UnrenderableLayersInfos = { diff --git a/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts b/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts index 89f3a391881..85b02afdd70 100644 --- a/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/view_mode_accessor.ts @@ -22,6 +22,7 @@ import { getPosition, getRotationInRadian } from "viewer/model/accessors/flycam_ import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; import type { Flycam, WebknossosState } from "viewer/store"; import Dimensions from "../dimensions"; +import { getTransformedVoxelSize } from "./dataset_layer_transformation_accessor"; export function getTDViewportSize(state: WebknossosState): [number, number] { const camera = state.viewModeData.plane.tdCamera; @@ -30,9 +31,14 @@ export function getTDViewportSize(state: WebknossosState): [number, number] { export function getTDViewZoom(state: WebknossosState) { const { width } = getInputCatcherRect(state, OrthoViews.TDView); const [viewplaneWidth] = getTDViewportSize(state); - const { factor } = state.dataset.dataSource.scale; + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const dsScaleFactor = transformedVoxelSize.factor; + // We only need to calculate scaleX as scaleY would have the same value. - const scaleX = viewplaneWidth / (width * factor[0]); + const scaleX = viewplaneWidth / (width * dsScaleFactor[0]); return scaleX; } export function getInputCatcherRect(state: WebknossosState, viewport: Viewport): Rect { @@ -110,7 +116,12 @@ function _calculateMaybeGlobalPos( const planeId = planeIdOpt || state.viewModeData.plane.activeViewport; const curGlobalPos = getPosition(state.flycam); const flycamRotation = getRotationInRadian(state.flycam); - const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + + const planeRatio = getBaseVoxelFactorsInUnit(transformedVoxelSize); 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. @@ -207,7 +218,11 @@ function _calculateMaybePlaneScreenPos( const flycamPosition = getPosition(state.flycam); const flycamRotation = getRotationInRadian(state.flycam); - const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const planeRatio = getBaseVoxelFactorsInUnit(transformedVoxelSize); const positionInViewportPerspective = calculateInViewportPos( globalPosition, @@ -249,7 +264,11 @@ function _calculateMaybeGlobalDelta( ): Vector3 | null | undefined { let position: Vector3; planeId = planeId || state.viewModeData.plane.activeViewport; - const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const planeRatio = getBaseVoxelFactorsInUnit(transformedVoxelSize); const diffX = delta.x * state.flycam.zoomStep; const diffY = delta.y * state.flycam.zoomStep; @@ -335,7 +354,9 @@ function _calculateGlobalDelta( } export function getDisplayedDataExtentInPlaneMode(state: WebknossosState) { - const planeRatio = getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale); + // Use untransformed scale, because bounding boxes are stored in untransformed space + const untransformedVoxelSize = state.dataset.dataSource.scale; + const planeRatio = getBaseVoxelFactorsInUnit(untransformedVoxelSize); const curGlobalCenterPos = getPosition(state.flycam); const extents = OrthoViewValuesWithoutTDView.map((orthoView) => getPlaneExtentInVoxelFromStore(state, state.flycam.zoomStep, orthoView), @@ -343,8 +364,7 @@ export function getDisplayedDataExtentInPlaneMode(state: WebknossosState) { const [xyExtent, yzExtent, xzExtent] = extents; const minExtent = 1; - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'val1' implicitly has an 'any' type. - const getMinExtent = (val1, val2) => + const getMinExtent = (val1: number, val2: number) => _.min([val1, val2].filter((v) => v >= minExtent)) || minExtent; const xMinExtent = getMinExtent(xyExtent[0], xzExtent[0]) * planeRatio[0]; @@ -387,6 +407,9 @@ export function getPlaneScalingFactor( flycam: Flycam, planeID: OrthoView, ): [number, number] { + /* + * Returns the extent for the given plane by dividing the actual with/height of the viewport by constants.VIEWPORT_WIDTH. + */ const [width, height] = getPlaneExtentInVoxelFromStore(state, flycam.zoomStep, planeID); return [width / constants.VIEWPORT_WIDTH, height / constants.VIEWPORT_WIDTH]; } diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts b/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts index fbb630016b7..b4826b816c7 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts @@ -40,6 +40,7 @@ import { } from "viewer/model/volumetracing/section_labeling"; import type { Mapping } from "viewer/store"; import Store from "viewer/store"; +import { getTransformedVoxelSize } from "../accessors/dataset_layer_transformation_accessor"; import type { MagInfo } from "../helpers/mag_info"; import { getConstructorForElementClass } from "../helpers/typed_buffer"; @@ -1094,7 +1095,12 @@ function checkLineIntersection(bentMesh: Mesh, pointAVec3: Vector3, pointBVec3: if (!geometry.boundsTree) { geometry.computeBoundsTree(); } - const scale = Store.getState().dataset.dataSource.scale.factor; + const state = Store.getState(); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const scale = transformedVoxelSize.factor; const pointA = new ThreeVector3(...V3.scale3(pointAVec3, scale)); const pointB = new ThreeVector3(...V3.scale3(pointBVec3, scale)); diff --git a/frontend/javascripts/viewer/model/helpers/nml_helpers.ts b/frontend/javascripts/viewer/model/helpers/nml_helpers.ts index 557d1ea3d40..53a0c69d88d 100644 --- a/frontend/javascripts/viewer/model/helpers/nml_helpers.ts +++ b/frontend/javascripts/viewer/model/helpers/nml_helpers.ts @@ -33,7 +33,10 @@ import type { WebknossosState, } from "viewer/store"; import { findGroup } from "viewer/view/right-border-tabs/trees_tab/tree_hierarchy_view_helpers"; -import { getTransformsForSkeletonLayer } from "../accessors/dataset_layer_transformation_accessor"; +import { + getTransformedVoxelSize, + getTransformsForSkeletonLayer, +} from "../accessors/dataset_layer_transformation_accessor"; import { getNodePosition } from "../accessors/skeletontracing_accessor"; import { min } from "./iterator_utils"; @@ -248,6 +251,12 @@ function serializeParameters( const userBBoxes = skeletonTracing.userBoundingBoxes; const taskBB = skeletonTracing.boundingBox; + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const voxelSize = applyTransform ? transformedVoxelSize : state.dataset.dataSource.scale; + return [ "", ...indent( @@ -260,10 +269,10 @@ function serializeParameters( wkUrl: `${location.protocol}//${location.host}`, }), serializeTag("scale", { - x: state.dataset.dataSource.scale.factor[0], - y: state.dataset.dataSource.scale.factor[1], - z: state.dataset.dataSource.scale.factor[2], - unit: state.dataset.dataSource.scale.unit, + x: voxelSize.factor[0], + y: voxelSize.factor[1], + z: voxelSize.factor[2], + unit: voxelSize.unit, }), serializeTag("offset", { x: 0, diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 55e090a7563..cbae039b78f 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -16,6 +16,7 @@ import Dimensions from "viewer/model/dimensions"; import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; import type { WebknossosState } from "viewer/store"; import { getUnifiedAdditionalCoordinates } from "../accessors/dataset_accessor"; +import { getTransformedVoxelSize } from "../accessors/dataset_layer_transformation_accessor"; function cloneMatrix(m: Matrix4x4): Matrix4x4 { return [ @@ -181,7 +182,11 @@ export function setDirectionReducer(state: WebknossosState, direction: Vector3) export function setRotationReducer(state: WebknossosState, rotation: Vector3) { if (state.dataset != null) { const [x, y, z] = rotation; - let matrix = resetMatrix(state.flycam.currentMatrix, state.dataset.dataSource.scale.factor); + let matrix = resetMatrix( + state.flycam.currentMatrix, + getTransformedVoxelSize(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName) + .factor, + ); 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]); @@ -206,7 +211,13 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState return update(state, { flycam: { currentMatrix: { - $set: resetMatrix(state.flycam.currentMatrix, action.dataset.dataSource.scale.factor), + $set: resetMatrix( + state.flycam.currentMatrix, + getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ).factor, + ), }, rotation: { $set: [0, 0, 0], @@ -300,6 +311,13 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState return setDirectionReducer(state, action.direction); } + case "UPDATE_DATASET_SETTING": { + if (action.propertyName === "nativelyRenderedLayerName") { + return setRotationReducer(state, state.flycam.rotation); + } + return state; + } + case "MOVE_FLYCAM": { if (action.vector.includes(Number.NaN)) { // if the action vector is invalid, do not update @@ -361,7 +379,9 @@ function FlycamReducer(state: WebknossosState, action: Action): WebknossosState ); deltaInWorld.set(...vector).applyMatrix4(flycamRotationMatrix); const zoomFactor = increaseSpeedWithZoom ? flycam.zoomStep : 1; - const scaleFactor = getBaseVoxelFactorsInUnit(dataset.dataSource.scale); + const scaleFactor = getBaseVoxelFactorsInUnit( + getTransformedVoxelSize(dataset, state.datasetConfiguration.nativelyRenderedLayerName), + ); let deltaInWorldZoomed = V3.multiply( V3.scale(deltaInWorld.toArray(), zoomFactor), scaleFactor, diff --git a/frontend/javascripts/viewer/model/sagas/flycam_info_cache_saga.ts b/frontend/javascripts/viewer/model/sagas/flycam_info_cache_saga.ts index d9d3f221412..f199a4de71f 100644 --- a/frontend/javascripts/viewer/model/sagas/flycam_info_cache_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/flycam_info_cache_saga.ts @@ -13,6 +13,7 @@ import AsyncGetMaximumZoomForAllMags from "viewer/workers/async_get_maximum_zoom import { createWorker } from "viewer/workers/comlink_wrapper"; import { getDataLayers, getMagInfo } from "../accessors/dataset_accessor"; import { + getTransformedVoxelSize, getTransformsForLayer, invertAndTranspose, } from "../accessors/dataset_layer_transformation_accessor"; @@ -100,13 +101,22 @@ export default function* maintainMaximumZoomForAllMagsSaga(): Saga { ).affineMatrix, ); - const dummyFlycamMatrix = _getDummyFlycamMatrix(state.dataset.dataSource.scale.factor); + const dummyFlycamMatrix = _getDummyFlycamMatrix( + getTransformedVoxelSize(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName) + .factor, + ); + + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + const dsScaleFactor = transformedVoxelSize.factor; const zoomLevels = yield* call( getZoomLevelsFn, viewMode, state.datasetConfiguration.loadingStrategy, - state.dataset.dataSource.scale.factor, + dsScaleFactor, getMagInfo(layer.mags).getDenseMags(), getViewportRects(state), Math.min( diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 74847f20d99..a3b4f106667 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -23,6 +23,7 @@ import { getMappingInfo, getVisibleSegmentationLayer, } from "viewer/model/accessors/dataset_accessor"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getActiveSegmentationTracing, getMeshInfoForSegment, @@ -483,7 +484,11 @@ function* maybeLoadMeshChunk( batchCounterPerSegment[segmentId]++; threeDMap.set(paddedPositionWithinLayer, true); - const scaleFactor = yield* select((state) => state.dataset.dataSource.scale.factor); + const scaleFactor = yield* select( + (state) => + getTransformedVoxelSize(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName) + .factor, + ); if (isInitialRequest) { sendAnalyticsEvent("request_isosurface", { diff --git a/frontend/javascripts/viewer/model/sagas/saga_selectors.ts b/frontend/javascripts/viewer/model/sagas/saga_selectors.ts index 8d99e161c7c..ff1a661c473 100644 --- a/frontend/javascripts/viewer/model/sagas/saga_selectors.ts +++ b/frontend/javascripts/viewer/model/sagas/saga_selectors.ts @@ -6,12 +6,17 @@ import type { Saga } from "viewer/model/sagas/effect-generators"; import { select } from "viewer/model/sagas/effect-generators"; import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; import type { WebknossosState } from "viewer/store"; +import { getTransformedVoxelSize } from "../accessors/dataset_layer_transformation_accessor"; export function* getHalfViewportExtentsInVx(activeViewport: OrthoView): Saga { const zoom = yield* select((state) => state.flycam.zoomStep); - const baseVoxelFactors = yield* select((state) => - Dimensions.transDim(getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), activeViewport), - ); + const baseVoxelFactors = yield* select((state) => { + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + return Dimensions.transDim(getBaseVoxelFactorsInUnit(transformedVoxelSize), activeViewport); + }); const viewportExtents = yield* select((state) => getPlaneExtentInVoxelFromStore(state, zoom, activeViewport), ); @@ -28,8 +33,12 @@ export function getHalfViewportExtentsInUnitFromState( activeViewport: OrthoView, ): Vector2 { const zoom = state.flycam.zoomStep; + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); const baseVoxelFactors = Dimensions.transDim( - getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), + getBaseVoxelFactorsInUnit(transformedVoxelSize), activeViewport, ); const viewportExtents = getPlaneExtentInVoxelFromStore(state, zoom, activeViewport); diff --git a/frontend/javascripts/viewer/model/scaleinfo.ts b/frontend/javascripts/viewer/model/scaleinfo.ts index e99d6fc5c9f..4a173628e69 100644 --- a/frontend/javascripts/viewer/model/scaleinfo.ts +++ b/frontend/javascripts/viewer/model/scaleinfo.ts @@ -23,8 +23,8 @@ export function voxelToVolumeInUnit( ); } -export function getBaseVoxelFactorsInUnit(voxelSize: VoxelSize): Vector3 { - const scaleFactor = voxelSize.factor; +export function getBaseVoxelFactorsInUnit(maybeTransformedVoxelSize: VoxelSize): Vector3 { + const scaleFactor = maybeTransformedVoxelSize.factor; // base voxel should be a cube with highest mag const baseVoxel = getBaseVoxelInUnit(scaleFactor); // scale factor to calculate the voxels in a certain @@ -32,27 +32,23 @@ export function getBaseVoxelFactorsInUnit(voxelSize: VoxelSize): Vector3 { return [baseVoxel / scaleFactor[0], baseVoxel / scaleFactor[1], baseVoxel / scaleFactor[2]]; } -export function getVoxelPerUnit(voxelSize: VoxelSize): Vector3 { - const voxelPerUnit = [0, 0, 0] as Vector3; - - for (let i = 0; i < 3; i++) { - voxelPerUnit[i] = 1 / voxelSize.factor[i]; - } - return voxelPerUnit; -} - -export function voxelToUnit(voxelSize: VoxelSize, posArray: Vector3): Vector3 { +export function voxelToUnit(maybeTransformedVoxelSize: VoxelSize, posArray: Vector3): Vector3 { const result = [0, 0, 0] as Vector3; for (let i = 0; i < 3; i++) { - result[i] = posArray[i] * voxelSize.factor[i]; + result[i] = posArray[i] * maybeTransformedVoxelSize.factor[i]; } return result; } -export function convertVoxelSizeToUnit(voxelSize: VoxelSize, newUnit: UnitShort): Vector3 { - const shortUnit = LongUnitToShortUnitMap[voxelSize.unit]; +export function convertVoxelSizeToUnit( + maybeTransformedVoxelSize: VoxelSize, + newUnit: UnitShort, +): Vector3 { + const shortUnit = LongUnitToShortUnitMap[maybeTransformedVoxelSize.unit]; const conversionFactor = UnitsMap[shortUnit] / UnitsMap[newUnit]; - const voxelSizeInNewUnit = voxelSize.factor.map((value) => value * conversionFactor) as Vector3; + const voxelSizeInNewUnit = maybeTransformedVoxelSize.factor.map( + (value) => value * conversionFactor, + ) as Vector3; return voxelSizeInNewUnit; } diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 37ed61981fa..6881c378bd6 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -17,7 +17,10 @@ import { } from "viewer/model/helpers/position_converter"; import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; import Store from "viewer/store"; -import { invertAndTranspose } from "../accessors/dataset_layer_transformation_accessor"; +import { + getTransformedVoxelSize, + invertAndTranspose, +} from "../accessors/dataset_layer_transformation_accessor"; import { type Transform, invertTransform, @@ -392,9 +395,13 @@ class SectionLabeler { const { brushSize } = state.userConfiguration; const radius = Math.round(brushSize / 2); // Use the baseVoxelFactors to scale the rectangle, otherwise it'll become deformed + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); const scale = this.get2DCoordinate( scaleGlobalPositionWithMagnificationFloat( - getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), + getBaseVoxelFactorsInUnit(transformedVoxelSize), this.activeMag, ), ); @@ -470,9 +477,13 @@ class SectionLabeler { Math.floor(floatingCoord2d[1] - height / 2), ]; const buffer2D = this.createVoxelBuffer2D(minCoord2d, width, height); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); // Use the baseVoxelFactors to scale the circle, otherwise it'll become an ellipse const [scaleX, scaleY] = - scale ?? this.get2DCoordinate(getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale)); + scale ?? this.get2DCoordinate(getBaseVoxelFactorsInUnit(transformedVoxelSize)); const setMap = (x: number, y: number) => { buffer2D.setValue(x, y, 1); @@ -697,9 +708,12 @@ export class TransformedSectionLabeler { } getCircleVoxelBuffer2D(position: Vector3): VoxelBuffer2D { - let scale = this.adaptScaleFn( - getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), + const state = Store.getState(); + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, ); + let scale = this.adaptScaleFn(getBaseVoxelFactorsInUnit(transformedVoxelSize)); return this.base.getCircleVoxelBuffer2D(position, scale); } diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index 6c8f019c046..1297aa7d0e1 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -58,8 +58,8 @@ export type Params = { } >; magnificationsCount: number; - voxelSizeFactor: Vector3; - voxelSizeFactorInverted: Vector3; + // voxelSizeFactor: Vector3; + // voxelSizeFactorInverted: Vector3; isOrthogonal: boolean; useInterpolation: boolean; tpsTransformPerLayer: Record; @@ -155,8 +155,8 @@ uniform uint hoveredUnmappedSegmentIdHigh; // For some reason, taking the dataset scale from the uniform results in imprecise // 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) %>; +uniform vec3 voxelSizeFactor; // = <%= 1 /*formatVector3AsVec3(voxelSizeFactor)*/ %>; +uniform vec3 voxelSizeFactorInverted; // = <%= 1 /*formatVector3AsVec3(voxelSizeFactorInverted)*/ %>; const vec4 fallbackGray = vec4(0.5, 0.5, 0.5, 1.0); const float bucketWidth = <%= bucketWidth %>; diff --git a/frontend/javascripts/viewer/store.ts b/frontend/javascripts/viewer/store.ts index 0735d340186..fbbfc3df387 100644 --- a/frontend/javascripts/viewer/store.ts +++ b/frontend/javascripts/viewer/store.ts @@ -648,7 +648,7 @@ export const combinedReducer = reduceReducers( ProofreadingReducer, TaskReducer, SaveReducer, - FlycamReducer, + FlycamReducer, // needs to be executed after the settings reducer because of UPDATE_DATASET_SETTING handling in flycam reducer FlycamInfoCacheReducer, ViewModeReducer, AnnotationReducer, diff --git a/frontend/javascripts/viewer/view/action-bar/download_modal_view.tsx b/frontend/javascripts/viewer/view/action-bar/download_modal_view.tsx index 23d13d01539..233338641bf 100644 --- a/frontend/javascripts/viewer/view/action-bar/download_modal_view.tsx +++ b/frontend/javascripts/viewer/view/action-bar/download_modal_view.tsx @@ -180,8 +180,9 @@ function estimateFileSize( } function formatSelectedScale(dataset: APIDataset, mag: Vector3) { - const magAdaptedScale = dataset.dataSource.scale.factor.map((f, i) => f * mag[i]); - const unit = dataset.dataSource.scale.unit; + const untransformedVoxelSize = dataset.dataSource.scale; + const magAdaptedScale = untransformedVoxelSize.factor.map((f, i) => f * mag[i]); + const unit = untransformedVoxelSize.unit; const scale = { factor: magAdaptedScale, unit } as VoxelSize; return formatScale(scale); } diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index 0c598f67bd8..5c1cb354279 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -154,7 +154,10 @@ import type { VolumeTracing, } from "viewer/store"; -import { globalToLayerTransformedPosition } from "viewer/model/accessors/dataset_layer_transformation_accessor"; +import { + getTransformedVoxelSize, + globalToLayerTransformedPosition, +} from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { deleteNodeAsUserAction } from "viewer/model/actions/skeletontracing_actions_with_effects"; import { type MutableNode, type Tree, TreeMap } from "viewer/model/types/tree_types"; import Store from "viewer/store"; @@ -169,7 +172,7 @@ type Props = { contextInfo: ContextMenuInfo; additionalCoordinates: AdditionalCoordinate[] | undefined; skeletonTracing: SkeletonTracing | null | undefined; - voxelSize: VoxelSize; + transformedVoxelSize: VoxelSize; visibleSegmentationLayer: APIDataLayer | null | undefined; dataset: APIDataset; currentMeshFile: APIMeshFileInfo | null | undefined; @@ -466,7 +469,7 @@ function getMeshItems( volumeTracing: VolumeTracing | null | undefined, contextInfo: ContextMenuInfo, visibleSegmentationLayer: APIDataLayer | null | undefined, - voxelSizeFactor: Vector3, + transformedVoxelSizeFactor: Vector3, meshFileMappingName: string | null | undefined, isRotated: boolean, ): MenuItemType[] { @@ -632,7 +635,7 @@ function getMeshItems( { key: "jump-to-mesh", onClick: () => { - const unscaledPosition = V3.divide3(meshIntersectionPosition, voxelSizeFactor); + const unscaledPosition = V3.divide3(meshIntersectionPosition, transformedVoxelSizeFactor); Actions.setPosition(Store.dispatch, unscaledPosition); }, label: "Jump to Position", @@ -652,7 +655,7 @@ function getNodeContextMenuOptions({ clickedNodeId, contextInfo, visibleSegmentationLayer, - voxelSize, + transformedVoxelSize, useLegacyBindings, volumeTracing, infoRows, @@ -697,7 +700,7 @@ function getNodeContextMenuOptions({ volumeTracing, contextInfo, visibleSegmentationLayer, - voxelSize.factor, + transformedVoxelSize.factor, currentMeshFile?.mappingName, isRotated, ); @@ -841,14 +844,22 @@ function getNodeContextMenuOptions({ disabled: activeNodeId == null || !areInSameTree || isTheSameNode, onClick: () => activeNodeId != null - ? measureAndShowLengthBetweenNodes(activeNodeId, clickedNodeId, voxelSize.unit) + ? measureAndShowLengthBetweenNodes( + activeNodeId, + clickedNodeId, + transformedVoxelSize.unit, + ) : null, label: "Path Length to this Node", }, { key: "measure-whole-tree-length", onClick: () => - measureAndShowFullTreeLength(clickedTree.treeId, clickedTree.name, voxelSize.unit), + measureAndShowFullTreeLength( + clickedTree.treeId, + clickedTree.name, + transformedVoxelSize.unit, + ), label: "Path Length of this Tree", }, allowUpdate @@ -1029,7 +1040,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] visibleSegmentationLayer, segmentIdAtPosition, dataset, - voxelSize, + transformedVoxelSize, currentMeshFile, currentConnectomeFile, mappingInfo, @@ -1472,7 +1483,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] volumeTracing, contextInfo, visibleSegmentationLayer, - voxelSize.factor, + transformedVoxelSize.factor, currentMeshFile?.mappingName, isRotated, ); @@ -1618,7 +1629,12 @@ function ContextMenuInner() { ); const skeletonTracing = useWkSelector((state) => state.annotation.skeleton); const volumeTracing = useWkSelector(getActiveSegmentationTracing); - const voxelSize = useWkSelector((state) => state.dataset.dataSource.scale); + const transformedVoxelSize = useWkSelector((state) => { + return getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + }); const activeTool = useWkSelector((state) => state.uiInformation.activeTool); const dataset = useWkSelector((state) => state.dataset); const allowUpdate = useWkSelector((state) => state.annotation.isUpdatingCurrentlyAllowed); @@ -1660,7 +1676,7 @@ function ContextMenuInner() { skeletonTracing, visibleSegmentationLayer, volumeTracing, - voxelSize, + transformedVoxelSize, activeTool, dataset, allowUpdate, @@ -1732,7 +1748,7 @@ function ContextMenuInner() { }; const magInfo = getMagInfo(visibleSegmentationLayer.mags); const layersFinestMag = magInfo.getFinestMag(); - const voxelSize = dataset.dataSource.scale; + const untransformedVoxelSize = dataset.dataSource.scale; try { const [segmentSize] = await getSegmentVolumes( @@ -1760,11 +1776,15 @@ function ContextMenuInner() { const boundingBoxInMag1 = getBoundingBoxInMag1(boundingBoxInRequestedMag, layersFinestMag); const boundingBoxTopLeftString = `(${boundingBoxInMag1.topLeft[0]}, ${boundingBoxInMag1.topLeft[1]}, ${boundingBoxInMag1.topLeft[2]})`; const boundingBoxSizeString = `(${boundingBoxInMag1.width}, ${boundingBoxInMag1.height}, ${boundingBoxInMag1.depth})`; - const volumeInUnit3 = voxelToVolumeInUnit(voxelSize, layersFinestMag, segmentSize); + const volumeInUnit3 = voxelToVolumeInUnit( + untransformedVoxelSize, + layersFinestMag, + segmentSize, + ); return [ - formatNumberToVolume(volumeInUnit3, LongUnitToShortUnitMap[voxelSize.unit]), + formatNumberToVolume(volumeInUnit3, LongUnitToShortUnitMap[untransformedVoxelSize.unit]), `${boundingBoxTopLeftString}, ${boundingBoxSizeString}`, - formatNumberToArea(surfaceArea, LongUnitToShortUnitMap[voxelSize.unit]), + formatNumberToArea(surfaceArea, LongUnitToShortUnitMap[untransformedVoxelSize.unit]), ]; } catch (_error) { const notFetchedMessage = "Could not be fetched."; @@ -1809,8 +1829,12 @@ function ContextMenuInner() { activeNode != null && positionToMeasureDistanceTo != null ? [ formatNumberToLength( - V3.scaledDist(getActiveNodePosition(), positionToMeasureDistanceTo, voxelSize.factor), - LongUnitToShortUnitMap[voxelSize.unit], + V3.scaledDist( + getActiveNodePosition(), + positionToMeasureDistanceTo, + transformedVoxelSize.factor, + ), + LongUnitToShortUnitMap[transformedVoxelSize.unit], ), formatLengthAsVx(V3.length(V3.sub(getActiveNodePosition(), positionToMeasureDistanceTo))), ] diff --git a/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx b/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx index 15e32811c4c..75a82c8a4ac 100644 --- a/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx +++ b/frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx @@ -12,6 +12,7 @@ import { useEffect, useRef } from "react"; import { useDispatch } from "react-redux"; import { LongUnitToShortUnitMap, type Vector3 } from "viewer/constants"; import getSceneController from "viewer/controller/scene_controller_provider"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getPosition, getRotationInRadian } from "viewer/model/accessors/flycam_accessor"; import { AnnotationTool } from "viewer/model/accessors/tool_accessor"; import { @@ -46,10 +47,14 @@ export default function DistanceMeasurementTooltip() { 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 transformedVoxelSize = useWkSelector((state) => { + return getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + }); + const untransformedVoxelSize = useWkSelector((state) => state.dataset.dataSource.scale); + const planeRatio = getBaseVoxelFactorsInUnit(transformedVoxelSize); const tooltipRef = useRef(null); const dispatch = useDispatch(); const { areaMeasurementGeometry, lineMeasurementGeometry } = getSceneController(); @@ -105,15 +110,15 @@ export default function DistanceMeasurementTooltip() { const { lineMeasurementGeometry } = getSceneController(); valueInVx = formatLengthAsVx(lineMeasurementGeometry.getDistance(notScalingFactor), 1); valueInMetricUnit = formatNumberToLength( - lineMeasurementGeometry.getDistance(voxelSize.factor), - LongUnitToShortUnitMap[voxelSize.unit], + lineMeasurementGeometry.getDistance(untransformedVoxelSize.factor), + LongUnitToShortUnitMap[untransformedVoxelSize.unit], ); } else if (activeTool === AnnotationTool.AREA_MEASUREMENT) { const { areaMeasurementGeometry } = getSceneController(); valueInVx = formatAreaAsVx(areaMeasurementGeometry.getArea(notScalingFactor), 1); valueInMetricUnit = formatNumberToArea( - areaMeasurementGeometry.getArea(voxelSize.factor), - LongUnitToShortUnitMap[voxelSize.unit], + areaMeasurementGeometry.getArea(untransformedVoxelSize.factor), + LongUnitToShortUnitMap[untransformedVoxelSize.unit], ); } diff --git a/frontend/javascripts/viewer/view/right-border-tabs/dataset_info_tab_view.tsx b/frontend/javascripts/viewer/view/right-border-tabs/dataset_info_tab_view.tsx index d8b7ff03ba2..ad7c57dca28 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/dataset_info_tab_view.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/dataset_info_tab_view.tsx @@ -42,6 +42,7 @@ import messages from "messages"; import type { EmptyObject } from "types/globals"; import { WkDevFlags } from "viewer/api/wk_dev"; import { mayEditAnnotationProperties } from "viewer/model/accessors/annotation_accessor"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { formatUserName } from "viewer/model/accessors/user_accessor"; import { getReadableNameForLayerName } from "viewer/model/accessors/volumetracing_accessor"; import { ensureHasNewestVersionAction } from "viewer/model/actions/save_actions"; @@ -188,6 +189,11 @@ export function DatasetExtentRow({ dataset }: { dataset: APIDataset }) { } export function VoxelSizeRow({ dataset }: { dataset: APIDataset }) { + const nativelyRenderedLayerName = useWkSelector( + (state) => state.datasetConfiguration.nativelyRenderedLayerName, + ); + const transformedVoxelSize = getTransformedVoxelSize(dataset, nativelyRenderedLayerName); + return ( Voxel size - {formatScale(dataset.dataSource.scale)} + {formatScale(transformedVoxelSize)} ); } diff --git a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx index 2f8f6c01a0c..4b15ec4cdeb 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx @@ -103,7 +103,7 @@ export function SegmentStatisticsModal({ const { dataset, annotation } = useWkSelector((state) => state); const magInfo = getMagInfo(visibleSegmentationLayer.mags); const layersFinestMag = magInfo.getFinestMag(); - const voxelSize = dataset.dataSource.scale; + const untransformedVoxelSize = dataset.dataSource.scale; // Omit checking that all prerequisites for segment stats (such as a segment index) are // met right here because that should happen before opening the modal. const storeInfoType = { @@ -168,7 +168,7 @@ export function SegmentStatisticsModal({ const boundingBoxInMag1 = getBoundingBoxInMag1(currentBoundingBox, layersFinestMag); const currentSegmentSizeInVx = segmentSizes[i]; const volumeInUnit3 = voxelToVolumeInUnit( - voxelSize, + untransformedVoxelSize, layersFinestMag, currentSegmentSizeInVx, ); @@ -185,12 +185,12 @@ export function SegmentStatisticsModal({ volumeInUnit3, formattedSize: formatNumberToVolume( volumeInUnit3, - LongUnitToShortUnitMap[voxelSize.unit], + LongUnitToShortUnitMap[untransformedVoxelSize.unit], ), surfaceAreaInUnit2, formattedSurfaceArea: formatNumberToArea( surfaceAreaInUnit2, - LongUnitToShortUnitMap[voxelSize.unit], + LongUnitToShortUnitMap[untransformedVoxelSize.unit], ), boundingBoxTopLeft: boundingBoxInMag1.topLeft, boundingBoxTopLeftAsString: `(${boundingBoxInMag1.topLeft.join(", ")})`, @@ -270,7 +270,7 @@ export function SegmentStatisticsModal({ tracingId || dataset.name, parentGroup, hasAdditionalCoords, - voxelSize, + untransformedVoxelSize, ) } okText="Export to CSV" diff --git a/frontend/javascripts/viewer/view/scalebar.tsx b/frontend/javascripts/viewer/view/scalebar.tsx index 7cc0641b3f6..2c9d39a1b6b 100644 --- a/frontend/javascripts/viewer/view/scalebar.tsx +++ b/frontend/javascripts/viewer/view/scalebar.tsx @@ -4,6 +4,7 @@ import { connect } from "react-redux"; import type { APIDataset } from "types/api_types"; import type { OrthoView } from "viewer/constants"; import constants, { Unicode, OrthoViews, LongUnitToShortUnitMap } from "viewer/constants"; +import { getTransformedVoxelSize } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getZoomValue } from "viewer/model/accessors/flycam_accessor"; import { getTDViewZoom, getViewportExtents } from "viewer/model/accessors/view_mode_accessor"; import { getBaseVoxelInUnit } from "viewer/model/scaleinfo"; @@ -16,6 +17,7 @@ type OwnProps = { }; type StateProps = { dataset: APIDataset; + nativelyRenderedLayerName: string | null; zoomValue: number; viewportWidthInPixels: number; viewportHeightInPixels: number; @@ -26,8 +28,10 @@ function convertPixelsToUnit( lengthInPixel: number, zoomValue: number, dataset: APIDataset, + nativelyRenderedLayerName: string | null, ): number { - return lengthInPixel * zoomValue * getBaseVoxelInUnit(dataset.dataSource.scale.factor); + const transformedVoxelSize = getTransformedVoxelSize(dataset, nativelyRenderedLayerName); + return lengthInPixel * zoomValue * getBaseVoxelInUnit(transformedVoxelSize.factor); } const getBestScalebarAnchorInNm = (lengthInNm: number): number => { @@ -52,10 +56,26 @@ const idealScalebarWidthFactor = 0.3; const maxScaleBarWidthFactor = 0.45; const minWidthToFillScalebar = 130; -function Scalebar({ zoomValue, dataset, viewportWidthInPixels, viewportHeightInPixels }: Props) { - const voxelSizeUnit = dataset.dataSource.scale.unit; - const viewportWidthInUnit = convertPixelsToUnit(viewportWidthInPixels, zoomValue, dataset); - const viewportHeightInUnit = convertPixelsToUnit(viewportHeightInPixels, zoomValue, dataset); +function Scalebar({ + zoomValue, + dataset, + viewportWidthInPixels, + viewportHeightInPixels, + nativelyRenderedLayerName, +}: Props) { + const voxelSizeUnit = getTransformedVoxelSize(dataset, nativelyRenderedLayerName).unit; + const viewportWidthInUnit = convertPixelsToUnit( + viewportWidthInPixels, + zoomValue, + dataset, + nativelyRenderedLayerName, + ); + const viewportHeightInUnit = convertPixelsToUnit( + viewportHeightInPixels, + zoomValue, + dataset, + nativelyRenderedLayerName, + ); const idealWidthInUnit = viewportWidthInUnit * idealScalebarWidthFactor; const scalebarWidthInUnit = getBestScalebarAnchorInNm(idealWidthInUnit); const scaleBarWidthFactor = Math.min( @@ -126,6 +146,7 @@ const mapStateToProps = (state: WebknossosState, ownProps: OwnProps): StateProps return { zoomValue, dataset: state.dataset, + nativelyRenderedLayerName: state.datasetConfiguration.nativelyRenderedLayerName, viewportWidthInPixels: width, viewportHeightInPixels: height, }; diff --git a/frontend/javascripts/viewer/view/voxel_pipette_tooltip.tsx b/frontend/javascripts/viewer/view/voxel_pipette_tooltip.tsx index 83d93b153e9..75936ee69be 100644 --- a/frontend/javascripts/viewer/view/voxel_pipette_tooltip.tsx +++ b/frontend/javascripts/viewer/view/voxel_pipette_tooltip.tsx @@ -11,7 +11,10 @@ import { getOrderedColorLayers, getVisibleSegmentationLayer, } from "viewer/model/accessors/dataset_accessor"; -import { globalToLayerTransformedPosition } from "viewer/model/accessors/dataset_layer_transformation_accessor"; +import { + getTransformedVoxelSize, + globalToLayerTransformedPosition, +} from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { getCurrentMagIndex, getPosition, @@ -60,9 +63,13 @@ export default function VoxelValueTooltip() { const additionalCoordinates = useWkSelector((state) => state.flycam.additionalCoordinates); const zoomStep = useWkSelector((state) => state.flycam.zoomStep); const globalMousePosition = useWkSelector((state) => getGlobalMousePositionFloating(state)); - const datasetScale = useWkSelector((state) => - getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), - ); + const baseVoxelFactorsInUnit = useWkSelector((state) => { + const transformedVoxelSize = getTransformedVoxelSize( + state.dataset, + state.datasetConfiguration.nativelyRenderedLayerName, + ); + return getBaseVoxelFactorsInUnit(transformedVoxelSize); + }); const tooltipRef = useRef(null); const dispatch = useDispatch(); const orthoView = useWkSelector((state) => state.viewModeData.plane.activeViewport); @@ -84,13 +91,21 @@ export default function VoxelValueTooltip() { flycamRotation, flycamPosition, orthoView, - datasetScale, + baseVoxelFactorsInUnit, zoomStep, ) ) { dispatch(setVoxelPipetteTooltipPinnedPositionAction(null)); } - }, [dispatch, pinnedPosition, flycamRotation, flycamPosition, orthoView, datasetScale, zoomStep]); + }, [ + dispatch, + pinnedPosition, + flycamRotation, + flycamPosition, + orthoView, + baseVoxelFactorsInUnit, + zoomStep, + ]); const colorLayers = useWkSelector((state) => getOrderedColorLayers(state.dataset, state.datasetConfiguration.colorLayerOrder),