Skip to content

Allow rotating ortho view #8614

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 147 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from 105 commits
Commits
Show all changes
147 commits
Select commit Hold shift + click to select a range
0d4925c
wip: enable rotation for ortho views
MichaelBuessemeyer May 6, 2025
d53dfdc
fix shader
MichaelBuessemeyer May 6, 2025
53fe573
WIP: make planes and cameras rotate accordingly
MichaelBuessemeyer May 6, 2025
a0effba
fix base rotation of xy viewport
MichaelBuessemeyer May 6, 2025
4696242
fix rotation of xz and yz viewports (three js multiplys matrices in p…
MichaelBuessemeyer May 6, 2025
1a98739
little cleanup & WIP fixing rendering issues
MichaelBuessemeyer May 7, 2025
e9a91ca
WIP fixing plane scaling
MichaelBuessemeyer May 7, 2025
4d46f9c
try to fix plane scale
MichaelBuessemeyer May 8, 2025
a645251
remove plane wrapping group again
MichaelBuessemeyer May 9, 2025
df0fdac
fix shearing & rotation for datasets with anisotropic scale
MichaelBuessemeyer May 9, 2025
9109d07
clean up scene graph hierarchy
MichaelBuessemeyer May 9, 2025
5eb8f24
small potential speedup avoiding object creation
MichaelBuessemeyer May 9, 2025
639e7e6
remove doublesided material setting for plane as not needed
MichaelBuessemeyer May 9, 2025
1d5d499
fix move tool in arbitrary orientation
MichaelBuessemeyer May 9, 2025
d6557d8
fix line measurement tool location & rendering
MichaelBuessemeyer May 12, 2025
a3ffdba
fix f-ing through dataset
MichaelBuessemeyer May 12, 2025
4f5967c
disable unsupported tools when rotated in first version
MichaelBuessemeyer May 12, 2025
9870d2e
fix new node rotation value calculation
MichaelBuessemeyer May 12, 2025
273a58c
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer May 13, 2025
dc5d301
fix rendering correct data by passing position offset to shader and c…
MichaelBuessemeyer May 14, 2025
813028f
remove unused method
MichaelBuessemeyer May 14, 2025
c51c6b4
make abstract tree tab a functional component
MichaelBuessemeyer May 14, 2025
bafa502
suppress rotation of setting active node when in ortho view mode
MichaelBuessemeyer May 14, 2025
c9a4b27
fix flickering when adjusting clipping distance
MichaelBuessemeyer May 15, 2025
07ba557
fix clipping distance offsetting of planes leading to rendering errors
MichaelBuessemeyer May 15, 2025
0cd92a3
add missing prop to arbitrary_view to be easily distinguishable to pl…
MichaelBuessemeyer May 15, 2025
0cbcb7f
less store.getState calls
MichaelBuessemeyer May 16, 2025
4c465ae
fix node positioning in plane while rotation is active
MichaelBuessemeyer May 19, 2025
263c06d
WIP fix node creation
MichaelBuessemeyer May 19, 2025
1344b28
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer May 19, 2025
09d10ad
disallow bbox creating from context menu when rotation is active
MichaelBuessemeyer May 19, 2025
fd17634
fix continuous node creation & some small typescript complaint
MichaelBuessemeyer May 19, 2025
76ce52b
disable some context menu actions when rotation is active
MichaelBuessemeyer May 19, 2025
11297c8
add rotated bucket prefetching
MichaelBuessemeyer May 19, 2025
58aa187
move back to old prefetcher and disable once rotation is active
MichaelBuessemeyer May 20, 2025
c0edcd1
fix dataset navigation via space
MichaelBuessemeyer May 20, 2025
a56fd85
delete apparently useless file
MichaelBuessemeyer May 20, 2025
9994dd3
fix node moving
MichaelBuessemeyer May 20, 2025
c12870c
make rotation copy button to rotation reset button
MichaelBuessemeyer May 20, 2025
4fab626
store rotation in url even in ortho mode
MichaelBuessemeyer May 20, 2025
d3ed2e1
add is rotated flycam accessor
MichaelBuessemeyer May 20, 2025
d451f8f
fix logging warning
MichaelBuessemeyer May 20, 2025
dca68a0
disable area measurement while rotated
MichaelBuessemeyer May 20, 2025
4ad0f8a
do not have duplicate area measurement points
MichaelBuessemeyer May 20, 2025
bdf526b
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer May 21, 2025
be60117
precompute inverted voxelSizeFactor to avoid floating point imprecisions
MichaelBuessemeyer May 21, 2025
e633eb2
fix flickering when changing clipping distance by flooring & remove u…
MichaelBuessemeyer May 21, 2025
f7b039b
format frontend
MichaelBuessemeyer May 21, 2025
bf9a1d3
fix ts errors
MichaelBuessemeyer May 21, 2025
6d1f468
fix frontend tests
MichaelBuessemeyer May 21, 2025
5627656
Merge branch 'master' into ortho-view-rotation-v2
MichaelBuessemeyer May 21, 2025
66d8ed6
remove debug logging
MichaelBuessemeyer May 22, 2025
3d4b137
Merge branch 'ortho-view-rotation-v2' of github.com:scalableminds/web…
MichaelBuessemeyer May 22, 2025
4741cda
fix create node api
MichaelBuessemeyer May 22, 2025
848ed1f
fix bucket picking by fixing applying flycam rotation to camera & obj…
MichaelBuessemeyer May 23, 2025
a8a5f38
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer May 23, 2025
0127413
clean up
MichaelBuessemeyer May 23, 2025
dee20f0
add rotation popover
MichaelBuessemeyer May 26, 2025
273317d
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer May 27, 2025
9e84db9
make rotation sliders more consistent
MichaelBuessemeyer May 27, 2025
101bbae
derive rotation value from rotation matrix upon keyboard rotation
MichaelBuessemeyer May 28, 2025
3b01693
reenable shader optimizations as long as no rotation is active
MichaelBuessemeyer May 28, 2025
e098961
delete rotation capable prefetcher
MichaelBuessemeyer May 28, 2025
b1953c9
make rotation button warning orange in case of viewing the dataset ro…
MichaelBuessemeyer May 28, 2025
8ca3d59
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer May 28, 2025
2d747e0
Reduce amount of THREE object created by creating them once and then …
MichaelBuessemeyer May 28, 2025
10ba989
sort imports
MichaelBuessemeyer May 28, 2025
7b4889e
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer Jun 2, 2025
0a8ec72
WIP apply feedback
MichaelBuessemeyer Jun 2, 2025
6162551
use antd internals danger coloring
MichaelBuessemeyer Jun 3, 2025
94f2e17
apply last feedback of review first cycle
MichaelBuessemeyer Jun 3, 2025
33fd149
fix imports
MichaelBuessemeyer Jun 3, 2025
a2cb726
undo distance measurement tooltip position refactoring
MichaelBuessemeyer Jun 3, 2025
6bc8092
omit rotation in url hash in case it is default
MichaelBuessemeyer Jun 5, 2025
8381421
improve comment & interface of createnode api function
MichaelBuessemeyer Jun 5, 2025
ea6cfce
readd using manual orange warning color for position & rotation view
MichaelBuessemeyer Jun 5, 2025
de5b42c
remove useless flooring of already rounded global position
MichaelBuessemeyer Jun 5, 2025
a828c70
rename globalposition to PositionWithRounding
MichaelBuessemeyer Jun 5, 2025
b94fe9a
mini renaming
MichaelBuessemeyer Jun 5, 2025
c6bf024
deduplicate test flycam matrix with default rotation of 180° around z…
MichaelBuessemeyer Jun 10, 2025
1db3461
add option to rotate to active node upon activation
MichaelBuessemeyer Jun 10, 2025
75b0a97
cleanup naming of getRotationOrthoInRadian function. Is now called ge…
MichaelBuessemeyer Jun 10, 2025
5fc859c
clean up getRelativeViewportRotationToXYViewport to move it as a mapp…
MichaelBuessemeyer Jun 10, 2025
901ccb3
make line measurement tool more accurate by using floating point and …
MichaelBuessemeyer Jun 10, 2025
d7b3828
little more clean up
MichaelBuessemeyer Jun 10, 2025
ccb0556
refactor rotations of planes and extract camera viewing direction cor…
MichaelBuessemeyer Jun 13, 2025
1115285
keep rotation values in popup and url equal to avoid confusion
MichaelBuessemeyer Jun 13, 2025
bdd7eef
maintain proper rotation values in store
MichaelBuessemeyer Jun 13, 2025
9188786
fix order in which ortho view depended rotation removed from active n…
MichaelBuessemeyer Jun 13, 2025
8b9fbe3
WIP try monkeypatching flippnig camera on node creation with 90,90,0 …
MichaelBuessemeyer Jun 13, 2025
f8a80ee
remove unused conmment
MichaelBuessemeyer Jun 14, 2025
fbda63e
WIP: fix 3d camera rotation buttons
MichaelBuessemeyer Jun 16, 2025
8fc80e1
WIP fix pattern rendering during rotation
MichaelBuessemeyer Jun 17, 2025
54817d7
WIP: fix pattern during rotation
MichaelBuessemeyer Jun 18, 2025
37be3e0
frontend import sorting
MichaelBuessemeyer Jun 18, 2025
19813ae
add 90 degree step rotation keyboard shortcut
MichaelBuessemeyer Jun 18, 2025
164623b
add reset all rotation button to rotation popover
MichaelBuessemeyer Jun 18, 2025
c0acca5
WIP: add tests to check for rotation prop in created nodes
MichaelBuessemeyer Jun 19, 2025
16c7b16
ensure getFlycamRotationWithAppendedRotation returns angles in range …
MichaelBuessemeyer Jun 19, 2025
f335325
fix default flycam rotation according to default flycam matrix
MichaelBuessemeyer Jun 26, 2025
0d815a0
fix url manager specs
MichaelBuessemeyer Jun 26, 2025
589dc32
fix cyclic dependencies in tests
MichaelBuessemeyer Jun 26, 2025
7230cc0
add todo comment & more debugging output
MichaelBuessemeyer Jun 26, 2025
1a4c4f8
fix node creation rotation test
MichaelBuessemeyer Jun 27, 2025
8ed14c4
fix calculation of node rotation by doing same quirky matrix math ope…
MichaelBuessemeyer Jun 27, 2025
e66461c
WIP: Fix 3d rotation buttons
MichaelBuessemeyer Jun 30, 2025
b02468c
refactor 3d rotation code
MichaelBuessemeyer Jun 30, 2025
37fcce3
WIP fix frontend tests
MichaelBuessemeyer Jun 30, 2025
c59b7a9
import sorting
MichaelBuessemeyer Jun 30, 2025
440f5ae
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer Jun 30, 2025
75b6146
add missing import
MichaelBuessemeyer Jun 30, 2025
9c44bd6
Merge branch 'master' into ortho-view-rotation-v2
MichaelBuessemeyer Jul 1, 2025
a062533
do not loop 90° rotation keyboard shortcuts and fix this rotation
MichaelBuessemeyer Jul 1, 2025
3d11c6d
fix typo
MichaelBuessemeyer Jul 1, 2025
8c6c929
small clean up here and there
MichaelBuessemeyer Jul 1, 2025
6c28cc0
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer Jul 2, 2025
b43849e
fix fly mode with rotation 0,0,0
MichaelBuessemeyer Jul 2, 2025
aead902
fix rotation applying when activating node
MichaelBuessemeyer Jul 2, 2025
feef58e
fix applying node rotation of node created in arbitrary view
MichaelBuessemeyer Jul 2, 2025
86a6788
add changelog entry
MichaelBuessemeyer Jul 3, 2025
87625f8
Merge branch 'master' into ortho-view-rotation-v2
MichaelBuessemeyer Jul 3, 2025
ade03c6
sort imports
MichaelBuessemeyer Jul 3, 2025
d114670
Merge branch 'ortho-view-rotation-v2' of github.com:scalableminds/web…
MichaelBuessemeyer Jul 3, 2025
9e51b91
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer Jul 7, 2025
25361cf
apply feedback
MichaelBuessemeyer Jul 7, 2025
3efebd4
improve and add node creation rotation tests
MichaelBuessemeyer Jul 8, 2025
99322d5
remove outdated todo comments
MichaelBuessemeyer Jul 8, 2025
d3aa4ff
fix & unify skeleton node creation tests
MichaelBuessemeyer Jul 8, 2025
5347062
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer Jul 9, 2025
5ce70c1
add doc comments
MichaelBuessemeyer Jul 9, 2025
c721989
add keyboard shortcuts docs
MichaelBuessemeyer Jul 9, 2025
18a9221
remove unused import
MichaelBuessemeyer Jul 9, 2025
ef2b1c0
fix distance measurement tooltip to stay active in an anisotropic sca…
MichaelBuessemeyer Jul 10, 2025
719d35f
move flycam accessors spec
MichaelBuessemeyer Jul 10, 2025
06e6cc6
add tests for calculateMaybeGlobalPos as well as calculating the resu…
MichaelBuessemeyer Jul 10, 2025
3f41568
fix imports
MichaelBuessemeyer Jul 10, 2025
e5f0f91
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer Jul 10, 2025
a1ea02c
fix proofreading test?
MichaelBuessemeyer Jul 11, 2025
c405b65
edit comment explaining why the outer vertices of the vertex moving o…
MichaelBuessemeyer Jul 11, 2025
77c64c2
add assertion to proofreading test that tool wasn't incorrectly switched
MichaelBuessemeyer Jul 11, 2025
4121bc3
add comment explaining default stored rotation of [0,0,180]
MichaelBuessemeyer Jul 11, 2025
01bbc9a
fix zoom to position
MichaelBuessemeyer Jul 14, 2025
c0c8966
Merge branch 'master' of github.com:scalableminds/webknossos into ort…
MichaelBuessemeyer Jul 14, 2025
704a111
remove debugging logging
MichaelBuessemeyer Jul 14, 2025
5a7b341
add new flycam action to ignored list
MichaelBuessemeyer Jul 14, 2025
3ecab95
Update frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts
MichaelBuessemeyer Jul 15, 2025
5682dbb
Merge branch 'master' into ortho-view-rotation-v2
MichaelBuessemeyer Jul 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/javascripts/libs/mjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ const V3 = {
prod(a: Vector3) {
return a[0] * a[1] * a[2];
},

multiply: scale3,
};

const V4 = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const settings: Partial<Record<keyof RecommendedConfiguration, string>> =
moveValue: "Move Value (nm/s)",
newNodeNewTree: "Single-node-tree mode (Soma clicking)",
centerNewNode: "Auto-center Nodes",
applyNodeRotationOnActivation: "Auto-rotate to Nodes",
highlightCommentedNodes: "Highlight Commented Nodes",
overrideNodeRadius: "Override Node Radius",
particleSize: "Particle Size",
Expand Down Expand Up @@ -159,7 +160,6 @@ instead. Only enable this option if you understand its effect. All layers will n
"The current position is outside of the dataset's bounding box. No data will be shown here.",
"tracing.out_of_task_bounds": "The current position is outside of the task's bounding box.",
"tracing.copy_position": "Copy position to clipboard",
"tracing.copy_rotation": "Copy rotation to clipboard",
"tracing.copy_sharing_link": "Copy sharing link to clipboard",
"tracing.tree_length_notification": (treeName: string, lengthInNm: string, lengthInVx: string) =>
`The tree ${treeName} has a total path length of ${lengthInNm} (${lengthInVx}).`,
Expand Down
115 changes: 114 additions & 1 deletion frontend/javascripts/test/api/api_skeleton_latest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ import { setTreeGroupsAction } from "viewer/model/actions/skeletontracing_action
import { userSettings } from "types/schemas/user_settings.schema";
import Store from "viewer/store";
import { vi, describe, it, expect, beforeEach } from "vitest";
import type { Vector3 } from "viewer/constants";
import { OrthoBaseRotations, OrthoViews, OrthoViewToNumber, type Vector3 } from "viewer/constants";
import { enforceSkeletonTracing } from "viewer/model/accessors/skeletontracing_accessor";
import { setViewportAction } from "viewer/model/actions/view_mode_actions";
import { setRotationAction } from "viewer/model/actions/flycam_actions";
import * as THREE from "three";

const toRadian = (arr: Vector3): Vector3 => [
THREE.MathUtils.degToRad(arr[0]),
THREE.MathUtils.degToRad(arr[1]),
THREE.MathUtils.degToRad(arr[2]),
];

// All the mocking is done in the helpers file, so it can be reused for both skeleton and volume API
describe("API Skeleton", () => {
Expand Down Expand Up @@ -299,4 +308,108 @@ describe("API Skeleton", () => {
true,
);
});
it<WebknossosTestContext>("should create skeleton nodes with correct properties", ({ api }) => {
Store.dispatch(setRotationAction([0, 0, 0]));
Store.dispatch(setViewportAction(OrthoViews.PLANE_XY));
api.tracing.createNode([10, 10, 10]);
const newNode = enforceSkeletonTracing(Store.getState().annotation)
.trees.getOrThrow(2)
.nodes.getOrThrow(4);
const propsToCheck = {
untransformedPosition: newNode.untransformedPosition,
additionalCoordinates: newNode.additionalCoordinates,
rotation: newNode.rotation,
viewport: newNode.viewport,
mag: newNode.mag,
};
expect(propsToCheck).toStrictEqual({
untransformedPosition: [10, 10, 10],
additionalCoordinates: [],
rotation: [0, 0, 0],
viewport: OrthoViewToNumber[OrthoViews.PLANE_XY],
mag: 0,
});
});
it<WebknossosTestContext>("should create skeleton nodes with correct properties", ({ api }) => {
Store.dispatch(setRotationAction([0, 0, 0]));
Store.dispatch(setViewportAction(OrthoViews.PLANE_YZ));
api.tracing.createNode([10, 10, 10]);
const newNode = enforceSkeletonTracing(Store.getState().annotation)
.trees.getOrThrow(2)
.nodes.getOrThrow(4);
const propsToCheck = {
untransformedPosition: newNode.untransformedPosition,
additionalCoordinates: newNode.additionalCoordinates,
rotation: newNode.rotation,
viewport: newNode.viewport,
mag: newNode.mag,
};
expect(propsToCheck).toStrictEqual({
untransformedPosition: [10, 10, 10],
additionalCoordinates: [],
rotation: [0, 270, 0],
viewport: OrthoViewToNumber[OrthoViews.PLANE_YZ],
mag: 0,
});
});

it<WebknossosTestContext>("should create skeleton nodes with correct properties", ({ api }) => {
Store.dispatch(setRotationAction([0, 0, 0]));
Store.dispatch(setViewportAction(OrthoViews.PLANE_XZ));
api.tracing.createNode([10, 10, 10]);
const newNode = enforceSkeletonTracing(Store.getState().annotation)
.trees.getOrThrow(2)
.nodes.getOrThrow(4);
const propsToCheck = {
untransformedPosition: newNode.untransformedPosition,
additionalCoordinates: newNode.additionalCoordinates,
rotation: newNode.rotation,
viewport: newNode.viewport,
mag: newNode.mag,
};
expect(propsToCheck).toStrictEqual({
untransformedPosition: [10, 10, 10],
additionalCoordinates: [],
rotation: [90, 0, 0],
viewport: OrthoViewToNumber[OrthoViews.PLANE_XZ],
mag: 0,
});
});
it<WebknossosTestContext>("should create skeleton nodes with correct rotation when flycam is rotated in XY viewport.", ({
api,
}) => {
const rotation = [20, 90, 10] as Vector3;
const rotationQuaternion = new THREE.Quaternion().setFromEuler(
new THREE.Euler(...toRadian(rotation), "ZYX"),
);
Store.dispatch(setRotationAction(rotation));
Store.dispatch(setViewportAction(OrthoViews.PLANE_XY));
api.tracing.createNode([10, 10, 10]);
const newNode = enforceSkeletonTracing(Store.getState().annotation)
.trees.getOrThrow(2)
.nodes.getOrThrow(4);
const newNodeQuaternion = new THREE.Quaternion().setFromEuler(
new THREE.Euler(...toRadian(newNode.rotation), "ZYX"),
);
expect(rotationQuaternion.angleTo(newNodeQuaternion)).toBeLessThan(0.000001);
});
it<WebknossosTestContext>("should create skeleton nodes with correct rotation when flycam is rotated in YZ viewport.", ({
api,
}) => {
const rotation = [20, 90, 0] as Vector3;
const rotationQuaternion = new THREE.Quaternion()
.setFromEuler(new THREE.Euler(...toRadian(rotation), "ZYX"))
// Apply viewport's default rotation.
.multiply(new THREE.Quaternion().setFromEuler(OrthoBaseRotations[OrthoViews.PLANE_YZ]));
Store.dispatch(setRotationAction(rotation));
Store.dispatch(setViewportAction(OrthoViews.PLANE_YZ));
api.tracing.createNode([10, 10, 10]);
const newNode = enforceSkeletonTracing(Store.getState().annotation)
.trees.getOrThrow(2)
.nodes.getOrThrow(4);
const newNodeQuaternion = new THREE.Quaternion().setFromEuler(
new THREE.Euler(...toRadian(newNode.rotation), "ZYX"),
);
expect(rotationQuaternion.angleTo(newNodeQuaternion)).toBeLessThan(0.000001);
});
});
57 changes: 55 additions & 2 deletions frontend/javascripts/test/controller/url_manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import defaultState from "viewer/default_state";
import update from "immutability-helper";
import DATASET from "../fixtures/dataset_server_object";
import _ from "lodash";
import { FlycamMatrixWithDefaultRotation } from "test/fixtures/flycam_object";

describe("UrlManager", () => {
it("should replace tracing in url", () => {
Expand Down Expand Up @@ -192,7 +193,7 @@ describe("UrlManager", () => {
additionalCoordinates: [],
mode,
zoomStep: 1.3,
rotation: [0, 0, 180] as Vector3 as Vector3,
rotation: [0, 0, 180] as Vector3,
};
const initialState = update(defaultState, {
temporaryConfiguration: {
Expand All @@ -206,10 +207,62 @@ describe("UrlManager", () => {
expect(UrlManager.parseUrlHash()).toEqual(urlState as Partial<UrlManagerState>);
});

it("should support hashes with active node id and without a rotation", () => {
location.hash = "#3705,5200,795,0,1.3,15";
const urlState = UrlManager.parseUrlHash();
expect(urlState).toStrictEqual({
position: [3705, 5200, 795],
mode: "orthogonal",
zoomStep: 1.3,
activeNode: 15,
});
});

it("should parse an empty rotation", () => {
location.hash = "#3584,3584,1024,0,2,0,0,0";
const urlState = UrlManager.parseUrlHash();
expect(urlState).toStrictEqual({
position: [3584, 3584, 1024],
mode: "orthogonal",
zoomStep: 2,
rotation: [0, 0, 0],
});
});

it("should parse a rotation and active node id correctly", () => {
location.hash = "#3334,3235,999,0,2,282,308,308,11";
const urlState = UrlManager.parseUrlHash();
expect(urlState).toStrictEqual({
position: [3334, 3235, 999],
mode: "orthogonal",
zoomStep: 2,
rotation: [282, 308, 308],
activeNode: 11,
});
});

it("should build default url in csv format", () => {
UrlManager.initialize();
const url = UrlManager.buildUrl();
expect(url).toBe("#0,0,0,0,1.3");
// The default state in the store does not include the rotation of 180 degrees around z axis which is always subtracted from the rotation.
// Thus, the rotation of 180 around z is present.
expect(url).toBe("#0,0,0,0,1.3,0,0,180");
});

it("should build csv url hash without rotation if it is [0,0,0]", () => {
const rotationMatrixWithDefaultRotation = FlycamMatrixWithDefaultRotation;
const initialState = update(defaultState, {
flycam: {
currentMatrix: {
$set: rotationMatrixWithDefaultRotation,
},
rotation: {
$set: [0, 0, 0],
},
},
});
const hash = `#${UrlManager.buildUrlHashCsv(initialState)}`;
expect(hash).toBe("#0,0,0,0,1.3");
});

it("The dataset name should be correctly extracted from view URLs", () => {
Expand Down
5 changes: 5 additions & 0 deletions frontend/javascripts/test/fixtures/flycam_object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { M4x4 } from "libs/mjs";

// Apply the default 180 z axis rotation to identity matrix as this is always applied on every flycam per default.
// This can be useful in tests to get a calculated rotation of [0, 0, 0]. Otherwise it would be [0, 0, 180].
export const FlycamMatrixWithDefaultRotation = M4x4.rotate(Math.PI, [0, 0, 1], M4x4.identity(), []);
8 changes: 8 additions & 0 deletions frontend/javascripts/test/fixtures/volumetracing_object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import update from "immutability-helper";
import { AnnotationTool } from "viewer/model/accessors/tool_accessor";
import Constants from "viewer/constants";
import defaultState from "viewer/default_state";
import { FlycamMatrixWithDefaultRotation } from "./flycam_object";

const volumeTracing = {
type: "volume",
Expand Down Expand Up @@ -105,4 +106,11 @@ export const initialState = update(defaultState, {
},
},
},
flycam: {
currentMatrix: {
// Apply the default 180 z axis rotation to get correct result in ortho related tests.
// This ensures the calculated flycam rotation is [0, 0, 0]. Otherwise it would be [0, 0, 180].
$set: FlycamMatrixWithDefaultRotation,
},
},
});
36 changes: 23 additions & 13 deletions frontend/javascripts/test/reducers/flycam_reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { UnitLong, OrthoViews } from "viewer/constants";
import update from "immutability-helper";
import {
getPosition,
getRotation,
getRotationInDegrees,
getUp,
getLeft,
getZoomedMatrix,
} from "viewer/model/accessors/flycam_accessor";
import * as FlycamActions from "viewer/model/actions/flycam_actions";
import FlycamReducer from "viewer/model/reducers/flycam_reducer";
import { FlycamMatrixWithDefaultRotation } from "test/fixtures/flycam_object";
import { describe, it, expect } from "vitest";

function equalWithEpsilon(a: number[], b: number[], epsilon = 1e-10) {
Expand All @@ -36,7 +37,9 @@ const initialState = {
flycam: {
zoomStep: 2,
additionalCoordinates: [],
currentMatrix: M4x4.identity(),
// Apply the default 180 z axis rotation to get correct result in ortho related tests.
// This makes the calculated flycam rotation to [0, 0, 0]. Otherwise it would be [0, 0, 180].
currentMatrix: FlycamMatrixWithDefaultRotation,
spaceDirectionOrtho: [1, 1, 1],
},
temporaryConfiguration: {
Expand All @@ -45,35 +48,42 @@ const initialState = {
};

describe("Flycam", () => {
// Removing the default rotation from the matrix to have an easy expected matrix. Else the scaled rotation matrix would be harder to test.
const stateWithoutDefaultFlycamRotation = update(initialState, {
flycam: { currentMatrix: { $set: M4x4.identity() } },
});
it("should calculate zoomed matrix", () => {
expect(Array.from(getZoomedMatrix(initialState.flycam))).toEqual([
expect(Array.from(getZoomedMatrix(stateWithoutDefaultFlycamRotation.flycam))).toEqual([
2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1,
]);
});

it("should move the flycam", () => {
const moveAction = FlycamActions.moveFlycamAction([1, 2, 3]);
const newState = FlycamReducer(initialState, moveAction);
equalWithEpsilon(getPosition(newState.flycam), [1, 2, 3]);
// Due to initial rotation of 180 degree around z axis, x and y values are inverted.
equalWithEpsilon(getPosition(newState.flycam), [-1, -2, 3]);
});

it("should move the flycam backwards", () => {
const moveAction = FlycamActions.moveFlycamAction([-1, -2, -3]);
const newState = FlycamReducer(initialState, moveAction);
equalWithEpsilon(getPosition(newState.flycam), [-1, -2, -3]);
// Due to initial rotation of 180 degree around z axis, x and y values are inverted.
equalWithEpsilon(getPosition(newState.flycam), [1, 2, -3]);
});

it("should move the flycam and move it again", () => {
const moveAction = FlycamActions.moveFlycamAction([1, 2, 3]);
let newState = FlycamReducer(initialState, moveAction);
newState = FlycamReducer(newState, moveAction);
equalWithEpsilon(getPosition(newState.flycam), [2, 4, 6]);
// Due to initial rotation of 180 degree around z axis, x and y values are inverted.
equalWithEpsilon(getPosition(newState.flycam), [-2, -4, 6]);
});

it("should set the rotation the flycam", () => {
const rotateAction = FlycamActions.setRotationAction([180, 0, 0]);
const newState = FlycamReducer(initialState, rotateAction);
equalWithEpsilon(getRotation(newState.flycam), [180, 0, 0]);
equalWithEpsilon(getRotationInDegrees(newState.flycam), [180, 0, 0]);
equalWithEpsilon(getUp(newState.flycam), [0, 1, -0]);
equalWithEpsilon(getLeft(newState.flycam), [-1, 0, 0]);
});
Expand All @@ -100,33 +110,33 @@ describe("Flycam", () => {
const rotateAction = FlycamActions.rotateFlycamAction(0.5 * Math.PI, [1, 1, 0]);
const newState = FlycamReducer(initialState, rotateAction);
equalWithEpsilon(getPosition(newState.flycam), [0, 0, 0]);
equalWithEpsilon(V3.floor(getRotation(newState.flycam)), [270, 315, 135]);
equalWithEpsilon(V3.floor(getRotationInDegrees(newState.flycam)), [270, 315, 315]);
});

it("should pitch the flycam", () => {
const rotateAction = FlycamActions.pitchFlycamAction(0.5 * Math.PI);
const newState = FlycamReducer(initialState, rotateAction);
equalWithEpsilon(getPosition(newState.flycam), [0, 0, 0]);
equalWithEpsilon(getRotation(newState.flycam), [270, 0, 180]);
equalWithEpsilon(getRotationInDegrees(newState.flycam), [270, 0, 0]);
});

it("should pitch the flycam with spherical cap radius", () => {
const rotateAction = FlycamActions.pitchFlycamAction(0.5 * Math.PI, true);
const newState = FlycamReducer(initialState, rotateAction);
equalWithEpsilon(getPosition(newState.flycam), [0, -200, -200]);
equalWithEpsilon(getRotation(newState.flycam), [270, 0, 180]);
equalWithEpsilon(getPosition(newState.flycam), [0, 200, -200]);
equalWithEpsilon(getRotationInDegrees(newState.flycam), [270, 0, 0]);
});

it("should yaw the flycam", () => {
const rotateAction = FlycamActions.yawFlycamAction(0.5 * Math.PI);
const newState = FlycamReducer(initialState, rotateAction);
equalWithEpsilon(getRotation(newState.flycam), [0, 270, 180]);
equalWithEpsilon(getRotationInDegrees(newState.flycam), [180, 270, 180]);
});

it("should roll the flycam", () => {
const rotateAction = FlycamActions.rollFlycamAction(0.5 * Math.PI);
const newState = FlycamReducer(initialState, rotateAction);
equalWithEpsilon(getRotation(newState.flycam), [0, 0, 90]);
equalWithEpsilon(getRotationInDegrees(newState.flycam), [0, 0, 270]);
});

it("should move in ortho mode", () => {
Expand Down
Loading