Skip to content

Commit 254d094

Browse files
Allow rotating ortho view (#8614)
## Documentation about rotation & 3d scene https://www.notion.so/scalableminds/3D-Rotations-3D-Scene-210b51644c6380c2a4a6f5f3c069738a This documentation explains the quirky of how rotations are handled in more details. ### URL of deployed dev instance (used for testing): - https://___.webknossos.xyz ### Steps to test: - Review the docs file above - UI - Check in the navbar the position input. Right to the text input, there should be a new button. - Hover that button. It is the new rotation pop over - Play round with the pop over, slide the sliders. No value jumping should occur. When the rotation changes, the viewports should adjust accordingly - Also use the manual number input - Try the reset buttons individually. This should reset only the axis's value to 0. - Try the reset all button. It should reset the whole rotation to 0,0,0 - Check the URL. When the rotation value changes, the rotation stored in the url hash should as well (deferred by 2 seconds or so) - Keyboard shortcuts: - Use the shortcuts to rotate the cameras / planes: shift + up/down/left/right & alt + left/right. Holding should continue the rotation. - Use the shortcuts to directly rotate by 90°. Same as above just with ctrl pressed additionally. Holding should !not! continue the rotation. - Tools - In view mode check the line measurement tool & use it. The area measurement tool should not be available once the rotation is no longer 0,0,0. - create an annotation of a dataset with anisotropic voxel scaling. - once a rotation is configured, all tools except for the line measurement tool and the skeleton tool should no longer be available. - test the line measurement tool again - Skeleton tool testing - try creating some nodes while a rotation is active. - create multiple trees. Each tree should be traced with a different rotation active. Moreover, also trace some in different viewports. - try moving nodes around - in the layers tab - skeleton- disable "Auto-rotate to Nodes" - activate some previously traced nodes. The rotation should not change. - activate the "Auto-rotate to Nodes" setting. Again activate the trees / nodes. The rotation should now change to the rotation is was previously when the tree was traced. - This can easily be detected when the nodes are close to each other. All nodes of the tree should be visible as they were tracing in the same planar orientation. The viewports in which the "plane of nodes" should be visible should be the same as during the tracing. - Deactivate the "Auto-center nodes" setting. Now again active the trees / nodes. The cameras should still rotate towards the nodes but not move there to center the active node. - splitting trees and so on should also still work - Reset the orientation. Give the volume tools a try. They should work as expected. - Proofreading. - create an annotation with a hdf5 (or zarr) mapping and go into the proofreading tool. - Now do some proofreading actions. This should work - configure some rotation. Again do some proofreading actions. Everything should still work as expected. - As the semantics of the getGlobalPosition from clicked position changed. Please give each other tracing tool a quick try whether it still works as expected. - 3D viewport - Please check the 3d viewport buttons. - Configure a rotation - Use the 4 orientation buttons of the 3d viewport. They should focus the clicked plane / look from the top-right corner down at the cross section. Just as when no rotaiton is configured. - Reset the rotation. Check the buttons again. They should behave as on the master. - Tabs - Give the segment, skeleton and so on tabs a quick little testing whether the basics still work. Just avoid unwanted easily catchable bugs. - Arbitrary view - Check the tracing in arbitrary view. rotating there should still work as well as creating nodes and so on. - Including flight & oblique mode - And of cause: Other things you thing might be broken by this PR - URL / Links - Please check that links inlude the rotation in the hash and that it is applied on page load. - Configure some rotation - Copy the full URL into another tab and view it - It should automatically show the configured rotation and the 3d viewport should still look like the 3d button of it was just clicked (-> looking at the planes correctly and not in wrong / weird way / angle) ### TODOs: - [x] fix rendering correct data in ortho viewports (avoid rendering data offset by clipping distance) - [x] Do not use rotation value of just activated skeleton node - [x] Discuss what the actual rotation value of a newly created node should be - [x] Fully adapt context menu (disabled options too complex to support in current interaction if rotation is active) - [x] fix scalebar info when rotated - ~~adapt bucket picking (reuse picker for arbitrary mode)~~ - deferred for now. see https://scm.slack.com/archives/C5AKLAV0B/p1747730073516239 - [x] Fix rendering wrong slice due to offset calculation floating point inaccuracies. - [x] Fix dataset navigation via space - [x] put rotation value into url - [x] adapt shader - optimizations are turned off when - ~~The URL zoom state is increasing with every created skeleton node xD~~ - Nope, my mistake. The active node id is part of the url - [x] Fix centering new nodes. Currently the animation slowly offsets the rotated plane backwards and thus multiple click lead to a "slice switch" -> thus the user stays not on the same plane. - [x] adapt mouse interactions - [x] mapping of mouse position to voxel position - [x] fix skeleton tool interactions - [x] move skeleton node - [x] fix measurement tools interactions - [x] Line measurement - [x] area measurement - A little more complicated: Deferred for now -> Disable while rotated. - [x] Fix brushing in wrong slice :// - [x] first iteration should only support skeleton tool - [x] Brushing is off by one - [x] UI - [x] disable all tools beside skeleton & measurement once rotation is active - [x] expose rotation value - [x] "warn" user when angles are not axis aligned - The rotation button in the position view now has an orange warning border in case the dataset is currently rotated - [x] Put correct buckets on GPU. - [x] active tool mouse cursor is somehow broken?! - Seemed to be browser hiccups - [x] Fix `dynamicSpaceDirection` setting while rotation is active - [x] Consider optimizing the code by avoiding creating new threejs objects and use mjs instead - [x] Create issue for rotation aware prefetcher & delete this prs rotation aware prefetcher try - See #8661 - [x] Check whether dynamic space orientation still works - "df switching" works - [x] Fix rotation UI: Some rotations are ambiguous and just sliding the sliders causes jumps in the sliders - [x] Fix measurement tooltip to not follow the screen while moving in plane - [x] disable or support rendering of segmentation data in rotated viewport (how to interpolate?) - works for now, not sure about interpolation - [x] fix pattern rendering - ~~its kinda fixed but the pattern does not rotate correctly with the data leading to unexpected "pattern moving" while rotating~~ - Accepted for now - [x] Interpolate active node rotation via quaternions; rotates pretty wonky currently - [x] Test rotating to nodes in arbitrary view mode - [x] Adapt 3d viewport rotation buttons - [x] 3d button does not yield the desired result - [x] No data is rendered in fly mode when rotation is exactly `[0, 0, 0]` - [x] Bug: Node selection + rotation activation leads to strange rotations being activated - [x] refactor `getUnrotatedWorldCoordUVW` - [x] On l4_sample with `#3598,3476,994,0,0.933,30,0,0,50` there can be a "black row" - see https://github.com/user-attachments/assets/9b8b43d7-7da2-4a8f-8718-edfd7e2778f9 - created an issue for this: #8745 - see #8614 (comment) - [x] test `ctrl + alt + left` and `ctrl + alt + right` keyboard shortcut for 90° rotation. Doesn't work on my system as it is a window manager reserved shortcut. - [x] Fix line measurement tool when rotation is active. - Is buggy in anisotropic datasets ### Issues: - fixes #7569 ------ (Please delete unneeded items, merge only when none are left open) - [x] Updated [changelog](../blob/master/CHANGELOG.unreleased.md#unreleased) - [ ] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) --------- Co-authored-by: Philipp Otto <philippotto@users.noreply.github.com>
1 parent 35a23d3 commit 254d094

File tree

71 files changed

+2148
-878
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+2148
-878
lines changed

docs/ui/keyboard_shortcuts.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,28 @@ A complete listing of all available keyboard & mouse shortcuts for WEBKNOSSOS ca
4040

4141
Note that skeleton-specific mouse actions are usually only available when the skeleton tool is active.
4242

43-
| Key Binding | Operation |
44-
| --------------------------------------------- | ------------------------------------------- |
45-
| Left Mouse Drag or Arrow Keys | Move In-Plane |
46-
| ++alt++ + Mouse Move | Move In-Plane |
47-
| ++space++ | Move Forward |
48-
| Scroll Mousewheel (3D View) | Zoom In And Out |
49-
| Right-Click Drag (3D View) | Rotate 3D View |
50-
| Left Click | Create New Node |
51-
| Left Click | Select Node (Mark as Active Node) under cursor |
52-
| Left Drag | Move node under cursor |
53-
| Right Click (on node) | Bring up the context-menu with further actions |
54-
| ++shift++ + ++alt++ + Left Click | Merge Two Nodes and Combine Trees |
55-
| ++shift++ + ++ctrl++ / ++cmd++ + Left Click | Delete Edge / Split Trees |
56-
| ++c++ | Create New Tree |
57-
| ++ctrl++ / ++cmd++ + ++period++ | Navigate to the next Node (Mark as Active) |
58-
| ++ctrl++ / ++cmd++ + ++comma++ | Navigate to previous Node (Mark as Active) |
59-
| ++ctrl++ / ++cmd++ + Left Click or ++ctrl++ / ++cmd++ + Arrow Keys | Move the Active Node |
60-
| ++del++ | Delete Node / Split Trees |
61-
| ++b++ | Mark Node as New Branchpoint |
62-
| ++j++ | Jump To Last Branchpoint |
63-
| ++s++ | Center Camera on Active Node |
43+
| Key Binding | Operation |
44+
| -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
45+
| Left Mouse Drag or Arrow Keys | Move In-Plane |
46+
| ++alt++ + Mouse Move | Move In-Plane |
47+
| ++space++ | Move Forward |
48+
| ++shift++ + ++up++ / ++down++ / ++left++ / ++right++<br>++alt++ + ++left++ / ++right++ | Rotate Planes |
49+
| ++ctrl++ / ++cmd++ + ++shift++ + ++up++ / ++down++ / ++left++ / ++right++<br>++ctrl++ / ++cmd++ + ++alt++ + ++left++ / ++right++ | Rotate Planes by 90° |
50+
| Right-Click Drag (3D View) | Rotate 3D View |
51+
| Left Click | Create New Node |
52+
| Left Click | Select Node (Mark as Active Node) under cursor |
53+
| Left Drag | Move node under cursor |
54+
| Right Click (on node) | Bring up the context-menu with further actions |
55+
| ++shift++ + ++alt++ + Left Click | Merge Two Nodes and Combine Trees |
56+
| ++shift++ + ++ctrl++ / ++cmd++ + Left Click | Delete Edge / Split Trees |
57+
| ++c++ | Create New Tree |
58+
| ++ctrl++ / ++cmd++ + ++period++ | Navigate to the next Node (Mark as Active) |
59+
| ++ctrl++ / ++cmd++ + ++comma++ | Navigate to previous Node (Mark as Active) |
60+
| ++ctrl++ / ++cmd++ + Left Click or ++ctrl++ / ++cmd++ + Arrow Keys | Move the Active Node |
61+
| ++del++ | Delete Node / Split Trees |
62+
| ++b++ | Mark Node as New Branchpoint |
63+
| ++j++ | Jump To Last Branchpoint |
64+
| ++s++ | Center Camera on Active Node |
6465

6566

6667
Note that you can enable *Classic Controls* which will behave slightly different and more explicit for the mouse actions:

frontend/javascripts/libs/mjs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,8 @@ const V3 = {
387387
prod(a: Vector3) {
388388
return a[0] * a[1] * a[2];
389389
},
390+
391+
multiply: scale3,
390392
};
391393

392394
const V4 = {

frontend/javascripts/messages.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const settings: Partial<Record<keyof RecommendedConfiguration, string>> =
2222
moveValue: "Move Value (nm/s)",
2323
newNodeNewTree: "Single-node-tree mode (Soma clicking)",
2424
centerNewNode: "Auto-center Nodes",
25+
applyNodeRotationOnActivation: "Auto-rotate to Nodes",
2526
highlightCommentedNodes: "Highlight Commented Nodes",
2627
overrideNodeRadius: "Override Node Radius",
2728
particleSize: "Particle Size",
@@ -159,7 +160,6 @@ instead. Only enable this option if you understand its effect. All layers will n
159160
"The current position is outside of the dataset's bounding box. No data will be shown here.",
160161
"tracing.out_of_task_bounds": "The current position is outside of the task's bounding box.",
161162
"tracing.copy_position": "Copy position to clipboard",
162-
"tracing.copy_rotation": "Copy rotation to clipboard",
163163
"tracing.copy_sharing_link": "Copy sharing link to clipboard",
164164
"tracing.tree_length_notification": (treeName: string, lengthInNm: string, lengthInVx: string) =>
165165
`The tree ${treeName} has a total path length of ${lengthInNm} (${lengthInVx}).`,

frontend/javascripts/test/api/api_skeleton_latest.spec.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,42 @@ import { setTreeGroupsAction } from "viewer/model/actions/skeletontracing_action
55
import { userSettings } from "types/schemas/user_settings.schema";
66
import Store from "viewer/store";
77
import { vi, describe, it, expect, beforeEach } from "vitest";
8-
import type { Vector3 } from "viewer/constants";
8+
import {
9+
OrthoBaseRotations,
10+
OrthoViewToNumber,
11+
OrthoViewValuesWithoutTDView,
12+
type Vector3,
13+
} from "viewer/constants";
914
import { enforceSkeletonTracing } from "viewer/model/accessors/skeletontracing_accessor";
15+
import { setViewportAction } from "viewer/model/actions/view_mode_actions";
16+
import { setRotationAction } from "viewer/model/actions/flycam_actions";
17+
import * as THREE from "three";
18+
import {
19+
eulerAngleToReducerInternalMatrix,
20+
reducerInternalMatrixToEulerAngle,
21+
} from "viewer/model/helpers/rotation_helpers";
22+
import testRotations from "test/fixtures/test_rotations";
23+
import { map3 } from "libs/utils";
24+
25+
const toRadian = (arr: Vector3): Vector3 => [
26+
THREE.MathUtils.degToRad(arr[0]),
27+
THREE.MathUtils.degToRad(arr[1]),
28+
THREE.MathUtils.degToRad(arr[2]),
29+
];
30+
31+
function applyRotationInFlycamReducerSpace(
32+
flycamRotationInRadian: Vector3,
33+
rotationToApply: THREE.Euler,
34+
): Vector3 {
35+
// eulerAngleToReducerInternalMatrix and reducerInternalMatrixToEulerAngle are tested in rotation_helpers.spec.ts.
36+
// Calculate expected rotation and make it a quaternion for equal comparison.
37+
const rotationMatrix = eulerAngleToReducerInternalMatrix(flycamRotationInRadian);
38+
const rotationMatrixWithViewport = rotationMatrix.multiply(
39+
new THREE.Matrix4().makeRotationFromEuler(rotationToApply),
40+
);
41+
const resultingAngle = reducerInternalMatrixToEulerAngle(rotationMatrixWithViewport);
42+
return resultingAngle;
43+
}
1044

1145
describe("API Skeleton", () => {
1246
beforeEach<WebknossosTestContext>(async (context) => {
@@ -298,4 +332,76 @@ describe("API Skeleton", () => {
298332
true,
299333
);
300334
});
335+
336+
it<WebknossosTestContext>("should create skeleton nodes with correct properties.", ({ api }) => {
337+
const flycamRotation = [0, 0, 0] as Vector3;
338+
for (const planeId of OrthoViewValuesWithoutTDView) {
339+
const rotationForComparison = applyRotationInFlycamReducerSpace(
340+
flycamRotation,
341+
OrthoBaseRotations[planeId],
342+
);
343+
const rotationQuaternion = new THREE.Quaternion().setFromEuler(
344+
new THREE.Euler(...rotationForComparison),
345+
);
346+
Store.dispatch(setRotationAction(flycamRotation));
347+
Store.dispatch(setViewportAction(planeId));
348+
api.tracing.createNode([10, 10, 10], { activate: true });
349+
const skeletonTracing = enforceSkeletonTracing(Store.getState().annotation);
350+
// Throw error if no node / tree is active by passing -1 as id.
351+
const newNode = skeletonTracing.trees
352+
.getOrThrow(skeletonTracing.activeTreeId || -1)
353+
.nodes.getOrThrow(skeletonTracing.activeNodeId || -1);
354+
const propsToCheck = {
355+
untransformedPosition: newNode.untransformedPosition,
356+
additionalCoordinates: newNode.additionalCoordinates,
357+
viewport: newNode.viewport,
358+
mag: newNode.mag,
359+
};
360+
expect(propsToCheck).toStrictEqual({
361+
untransformedPosition: [10, 10, 10],
362+
additionalCoordinates: [],
363+
viewport: OrthoViewToNumber[planeId],
364+
mag: 0,
365+
});
366+
const newNodeQuaternion = new THREE.Quaternion().setFromEuler(
367+
new THREE.Euler(...toRadian(newNode.rotation)),
368+
);
369+
expect(
370+
rotationQuaternion.angleTo(newNodeQuaternion),
371+
`Node rotation ${newNode.rotation} is not nearly equal to ${map3(THREE.MathUtils.radToDeg, rotationForComparison)} in viewport ${planeId}.`,
372+
).toBeLessThan(0.000001);
373+
}
374+
});
375+
it<WebknossosTestContext>("should create skeleton nodes with correct rotation when flycam is rotated in all three viewports.", ({
376+
api,
377+
}) => {
378+
for (const testRotation of testRotations) {
379+
for (const planeId of OrthoViewValuesWithoutTDView) {
380+
const rotationInRadian = toRadian(testRotation);
381+
const resultingAngle = applyRotationInFlycamReducerSpace(
382+
rotationInRadian,
383+
OrthoBaseRotations[planeId],
384+
);
385+
const rotationQuaternion = new THREE.Quaternion().setFromEuler(
386+
new THREE.Euler(...resultingAngle),
387+
);
388+
// Test node creation.
389+
Store.dispatch(setRotationAction(testRotation));
390+
Store.dispatch(setViewportAction(planeId));
391+
api.tracing.createNode([10, 10, 10], { activate: true });
392+
const skeletonTracing = enforceSkeletonTracing(Store.getState().annotation);
393+
// Throw error if no node / tree is active by passing -1 as id.
394+
const newNode = skeletonTracing.trees
395+
.getOrThrow(skeletonTracing.activeTreeId || -1)
396+
.nodes.getOrThrow(skeletonTracing.activeNodeId || -1);
397+
const newNodeQuaternion = new THREE.Quaternion().setFromEuler(
398+
new THREE.Euler(...toRadian(newNode.rotation)),
399+
);
400+
expect(
401+
rotationQuaternion.angleTo(newNodeQuaternion),
402+
`Node rotation ${newNode.rotation} is not nearly equal to ${map3(THREE.MathUtils.radToDeg, resultingAngle)} in viewport ${planeId}.`,
403+
).toBeLessThan(0.000001);
404+
}
405+
}
406+
});
301407
});

frontend/javascripts/test/controller/url_manager.spec.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import defaultState from "viewer/default_state";
1212
import update from "immutability-helper";
1313
import DATASET from "../fixtures/dataset_server_object";
1414
import _ from "lodash";
15+
import { FlycamMatrixWithDefaultRotation } from "test/fixtures/flycam_object";
1516

1617
describe("UrlManager", () => {
1718
it("should replace tracing in url", () => {
@@ -192,7 +193,7 @@ describe("UrlManager", () => {
192193
additionalCoordinates: [],
193194
mode,
194195
zoomStep: 1.3,
195-
rotation: [0, 0, 180] as Vector3 as Vector3,
196+
rotation: [0, 0, 180] as Vector3,
196197
};
197198
const initialState = update(defaultState, {
198199
temporaryConfiguration: {
@@ -206,10 +207,62 @@ describe("UrlManager", () => {
206207
expect(UrlManager.parseUrlHash()).toEqual(urlState as Partial<UrlManagerState>);
207208
});
208209

210+
it("should support hashes with active node id and without a rotation", () => {
211+
location.hash = "#3705,5200,795,0,1.3,15";
212+
const urlState = UrlManager.parseUrlHash();
213+
expect(urlState).toStrictEqual({
214+
position: [3705, 5200, 795],
215+
mode: "orthogonal",
216+
zoomStep: 1.3,
217+
activeNode: 15,
218+
});
219+
});
220+
221+
it("should parse an empty rotation", () => {
222+
location.hash = "#3584,3584,1024,0,2,0,0,0";
223+
const urlState = UrlManager.parseUrlHash();
224+
expect(urlState).toStrictEqual({
225+
position: [3584, 3584, 1024],
226+
mode: "orthogonal",
227+
zoomStep: 2,
228+
rotation: [0, 0, 0],
229+
});
230+
});
231+
232+
it("should parse a rotation and active node id correctly", () => {
233+
location.hash = "#3334,3235,999,0,2,282,308,308,11";
234+
const urlState = UrlManager.parseUrlHash();
235+
expect(urlState).toStrictEqual({
236+
position: [3334, 3235, 999],
237+
mode: "orthogonal",
238+
zoomStep: 2,
239+
rotation: [282, 308, 308],
240+
activeNode: 11,
241+
});
242+
});
243+
209244
it("should build default url in csv format", () => {
210245
UrlManager.initialize();
211246
const url = UrlManager.buildUrl();
212-
expect(url).toBe("#0,0,0,0,1.3");
247+
// The default state in the store does not include the rotation of 180 degrees around z axis which is always subtracted from the rotation.
248+
// Thus, the rotation of 180 around z is present.
249+
expect(url).toBe("#0,0,0,0,1.3,0,0,180");
250+
});
251+
252+
it("should build csv url hash without rotation if it is [0,0,0]", () => {
253+
const rotationMatrixWithDefaultRotation = FlycamMatrixWithDefaultRotation;
254+
const initialState = update(defaultState, {
255+
flycam: {
256+
currentMatrix: {
257+
$set: rotationMatrixWithDefaultRotation,
258+
},
259+
rotation: {
260+
$set: [0, 0, 0],
261+
},
262+
},
263+
});
264+
const hash = `#${UrlManager.buildUrlHashCsv(initialState)}`;
265+
expect(hash).toBe("#0,0,0,0,1.3");
213266
});
214267

215268
it("The dataset name should be correctly extracted from view URLs", () => {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { M4x4 } from "libs/mjs";
2+
3+
// Apply the default 180 z axis rotation to identity matrix as this is always applied on every flycam per default.
4+
// This can be useful in tests to get a calculated rotation of [0, 0, 0]. Otherwise it would be [0, 0, 180].
5+
export const FlycamMatrixWithDefaultRotation = M4x4.rotate(Math.PI, [0, 0, 1], M4x4.identity(), []);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Vector3 } from "viewer/constants";
2+
3+
const testRotations: Vector3[] = [
4+
[0, 0, 0],
5+
[83, 0, 0],
6+
[10, 10, 10],
7+
[90, 160, 90],
8+
[30, 90, 40],
9+
[90, 90, 90],
10+
[180, 180, 180],
11+
[30, 30, 30],
12+
[90, 90, 188],
13+
[333, 222, 111],
14+
[73, 400, 666],
15+
];
16+
export default testRotations;

frontend/javascripts/test/fixtures/volumetracing_object.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import update from "immutability-helper";
22
import Constants from "viewer/constants";
33
import defaultState from "viewer/default_state";
4+
import { FlycamMatrixWithDefaultRotation } from "./flycam_object";
45
import { combinedReducer } from "viewer/store";
56
import { setDatasetAction } from "viewer/model/actions/dataset_actions";
67
import { convertFrontendBoundingBoxToServer } from "viewer/model/reducers/reducer_helpers";
@@ -110,6 +111,13 @@ const stateWithoutDatasetInitialization = update(defaultState, {
110111
},
111112
},
112113
},
114+
flycam: {
115+
currentMatrix: {
116+
// Apply the default 180 z axis rotation to get correct result in ortho related tests.
117+
// This ensures the calculated flycam rotation is [0, 0, 0]. Otherwise it would be [0, 0, 180].
118+
$set: FlycamMatrixWithDefaultRotation,
119+
},
120+
},
113121
});
114122

115123
export const initialState = combinedReducer(

frontend/javascripts/test/libs/transform_spec_helpers.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ export function almostEqual(
66
vec1: Vector3,
77
vec2: Vector3,
88
threshold: number = 1,
9+
message?: string,
910
) {
1011
const diffX = Math.abs(vec1[0] - vec2[0]);
1112
const diffY = Math.abs(vec1[1] - vec2[1]);
1213
const diffZ = Math.abs(vec1[2] - vec2[2]);
1314

14-
expect(diffX).toBeLessThan(threshold);
15-
expect(diffY).toBeLessThan(threshold);
16-
expect(diffZ).toBeLessThan(threshold);
15+
expect(diffX, message).toBeLessThan(threshold);
16+
expect(diffY, message).toBeLessThan(threshold);
17+
expect(diffZ, message).toBeLessThan(threshold);
1718
}
1819

1920
export function getPointsC555() {

0 commit comments

Comments
 (0)