diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/webxr/hit-test | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webxr/hit-test')
11 files changed, 1138 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webxr/hit-test/META.yml b/testing/web-platform/tests/webxr/hit-test/META.yml new file mode 100644 index 0000000000..e3f94f05a1 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/META.yml @@ -0,0 +1 @@ +spec: https://immersive-web.github.io/hit-test/ diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_source_cancel.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_source_cancel.https.html new file mode 100644 index 0000000000..5e7381df02 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_source_cancel.https.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// at world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 0, 0], + orientation: [0, 0, 0, 1], +}; + +// at world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, 0, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka mojo_from_floor + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // webxr_test_constants_fake_world.js has detailed description of the fake world +}; + +const test_function_generator = function(isTransientTest, isSessionEndedTest) { + return function(session, fakeDeviceController, t) { + + let done = false; + + return session.requestReferenceSpace('local').then((localSpace) => { + const validation_function = (hitTestSource) => { + + const rAFcb = function(time, frame) { + if(isSessionEndedTest) { + // Session is marked as "ended" synchronously, there is no need to + // wait for the promise it returns to settle. + session.end(); + + // Currently, the specification does not say what happen + // when a hit test source gets cancelled post-session-end. + hitTestSource.cancel(); + done = true; + } else { + hitTestSource.cancel(); + t.step(() => { + assert_throws_dom("InvalidStateError", () => hitTestSource.cancel()); + }); + done = true; + } + }; + + session.requestAnimationFrame(rAFcb); + + return t.step_wait(() => done); + }; + + // Same validation will happen both in transient and non-transient variant + if(isTransientTest) { + return session.requestHitTestSourceForTransientInput({ + profile: "generic-touchscreen", + offsetRay: new XRRay(), + }).then(validation_function); + } else { + return session.requestHitTestSource({ + space: localSpace, + offsetRay: new XRRay(), + }).then(validation_function); + } + }); // return session.requestReferenceSpace('local').then((localSpace) => { ... + }; // return function(session, fakeDeviceController, t) { ... +} + +xr_session_promise_test("Ensures hit test source cancellation works when the session has not ended.", + test_function_generator(/*isTransientTest=*/false, /*isSessionEndedTest=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Ensures transient input hit test source cancellation works when the session has not ended.", + test_function_generator(/*isTransientTest=*/true, /*isSessionEndedTest=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Ensures hit test source cancellation works when the session has ended", + test_function_generator(/*isTransientTest=*/false, /*isSessionEndedTest=*/true), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Ensures transient input hit test source cancellation works when the session has ended", + test_function_generator(/*isTransientTest=*/true, /*isSessionEndedTest=*/true), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_inputSources.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_inputSources.https.html new file mode 100644 index 0000000000..ca92d390fc --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_inputSources.https.html @@ -0,0 +1,170 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, -0.25, 0], + orientation: [0, 0, 0, 1], +}; + +// Start the screen pointer at the same place as the viewer, so it's essentially +// coming straight forward from the middle of the screen. +const SCREEN_POINTER_TRANSFORM = VIEWER_ORIGIN_TRANSFORM; + +const screen_controller_init = { + handedness: "none", + targetRayMode: "screen", + pointerOrigin: SCREEN_POINTER_TRANSFORM, // aka mojo_from_pointer + profiles: ["generic-touchscreen",] +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka floor_from_mojo + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // see webxr_test_constants_fake_world.js for details +}; + +// Generates a test function given the parameters for the hit test. +// |ray| - ray that will be used to subscribe to hit test. +// |expectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +// |inputFromPointer| - input from pointer transform that will be used as the input source's +// inputFromPointer (aka pointer origin) in subsequent rAF. +// |nextFrameExpectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +let testFunctionGenerator = function(ray, expectedPoses, inputFromPointer, nextFrameExpectedPoses) { + const testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('local').then((localRefSpace) => new Promise((resolve, reject) => { + + const input_source_controller = fakeDeviceController.simulateInputSourceConnection(screen_controller_init); + + requestSkipAnimationFrame(session, (time, frame) => { + t.step(() => { + assert_equals(session.inputSources.length, 1); + }); + + const input_source = session.inputSources[0]; + const hitTestOptionsInit = { + space: input_source.targetRaySpace, + offsetRay: ray, + }; + + session.requestHitTestSource(hitTestOptionsInit).then((hitTestSource) => { + t.step(() => { + assert_not_equals(hitTestSource, null); + }); + + // We got a hit test source, now get the results in subsequent rAFcb: + session.requestAnimationFrame((time, frame) => { + const results = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_equals(results.length, expectedPoses.length); + for(const [index, expectedPose] of expectedPoses.entries()) { + const pose = results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "before-move-pose: "); + } + } + }); + + input_source_controller.setPointerOrigin(inputFromPointer, false); + + session.requestAnimationFrame((time, frame) => { + const results = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_equals(results.length, nextFrameExpectedPoses.length); + for(const [index, expectedPose] of nextFrameExpectedPoses.entries()) { + const pose = results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "after-move-pose: "); + } + } + }); + + resolve(); + }); + }); + }); + }); + })); + }; + + return testFunction; +}; + + +// Pose of the first expected hit test result - straight ahead of the input source, viewer-facing. +const pose_1 = { + position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // Hit test API will set Y axis to the surface normal at the intersection point, + // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. + // If the surface normal and Z axis would be parallel, the hit test API + // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis + // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. + // In this particular case, `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - no move", + testFunctionGenerator(new XRRay(), [pose_1], SCREEN_POINTER_TRANSFORM, [pose_1]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const moved_pointer_transform_1 = { + position: [0, 1, 0], + orientation: [ 0.707, 0, 0, 0.707 ] // 90 degrees around X axis = facing up +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - after move - no results", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_1, []), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const pose_2 = { + position: {x: -1.443, y: 1.0, z: -2.5, w: 1.0}, + // Intersection point will be on the same height as the viewer, on the front + // wall. Distance from the front wall to viewer is 2.5m, and we are rotating + // to the left, so X coordinate of the intersection point will be negative + // & equal to -2.5 * tan(30 deg) ~= 1.443m. + orientation: {x: 0.5, y: 0.5, z: 0.5, w: 0.5 }, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y axis facing towards world's + // positive Z axis ([0,0,1]), Z axis to the right ([1,0,0]) and X axis + // towards world's Y axis ([0,1,0]). + // This is equivalent to the rotation around [1, 1, 1] vector by 120 degrees. +}; + +const moved_pointer_transform_2 = { + position: [0, 1, 0], + orientation: [ 0, 0.2588, 0, 0.9659 ] // 30 degrees around Y axis = to the left, + // creating 30-60-90 triangle with the front wall +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - after move - 1 result", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_2, [pose_2]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html new file mode 100644 index 0000000000..a30e71949c --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html @@ -0,0 +1,158 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, 0.25, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka mojo_from_floor + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // webxr_test_constants_fake_world.js has detailed description of the fake world +}; + +// Generates a test function given the parameters for the hit test. +// |ray| - ray that will be used to subscribe to hit test. +// |expectedPoses| - array of expected pose objects. The poses are expected to be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +// |refSpaceName| - XRReferenceSpaceType - either 'local', 'local-floor' or 'viewer'. +let testFunctionGenerator = function(ray, expectedPoses, refSpaceName) { + const testFunction = function(session, fakeDeviceController, t) { + return Promise.all([ + session.requestReferenceSpace('local'), + session.requestReferenceSpace('viewer'), + session.requestReferenceSpace('local-floor'), + ]).then(([localRefSpace, viewerRefSpace, localFloorRefSpace]) => { + + const refSpaceNameToSpace = { + 'local' : localRefSpace, + 'viewer' : viewerRefSpace, + 'local-floor' : localFloorRefSpace + }; + + const hitTestOptionsInit = { + space: refSpaceNameToSpace[refSpaceName], + offsetRay: ray, + }; + + return session.requestHitTestSource(hitTestOptionsInit).then( + (hitTestSource) => new Promise((resolve, reject) => { + + const requestAnimationFrameCallback = function(time, frame) { + const hitTestResults = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_equals(hitTestResults.length, expectedPoses.length, "Results length should match expected results length"); + for(const [index, expectedPose] of expectedPoses.entries()) { + const pose = hitTestResults[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose); + } + } + }); + + resolve(); + }; + + t.step(() => { + assert_true(hitTestSource != null, "Hit test source should not be null"); + }); + + session.requestAnimationFrame(requestAnimationFrameCallback); + })); + }); + }; + + return testFunction; +}; + +// Generates a test function that will use local space for hit test subscription. +// See testFunctionGenerator for explanation of other parameters. +const localBasedTestFunctionGenerator = function(ray, expectedPoses) { + return testFunctionGenerator(ray, expectedPoses, 'local'); +}; + +// Generates a test function that will use viewer space for hit test subscription. +// See testFunctionGenerator for explanation of other parameters. +const viewerBasedTestFunctionGenerator = function(ray, expectedPoses) { + return testFunctionGenerator(ray, expectedPoses, 'viewer'); +}; + +// Generates a test function that will use local-floor space for hit test subscription. +// See testFunctionGenerator for explanation of other parameters. +const localFloorBasedTestFunctionGenerator = function(ray, expectedPoses) { + return testFunctionGenerator(ray, expectedPoses, 'local-floor'); +}; + +// All test cases require local-floor and hit-test. +const sessionInit = { 'requiredFeatures': ['local-floor', 'hit-test'] }; + +// Pose of the first expected hit test result - straight ahead of the viewer, viewer-facing. +const pose_1 = { + position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // Hit test API will set Y axis to the surface normal at the intersection point, + // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. + // If the surface normal and Z axis would be parallel, the hit test API + // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis + // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. + // In this particular case, `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test( + "Ensures subscription to hit test works with viewer space - straight ahead - plane", + viewerBasedTestFunctionGenerator(new XRRay(), [pose_1]), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +xr_session_promise_test("Ensures subscription to hit test works with viewer space - straight up - no results", + viewerBasedTestFunctionGenerator(new XRRay({}, {x: 0.0, y: 1.0, z : 0.0}), []), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +const pose_2 = { + position: {x: 0.0, y: 0.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y and Z axis towards the ray origin so it won't be used, + // but `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to hit test works with local space", + localBasedTestFunctionGenerator(new XRRay(), [pose_2]), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +const pose_3 = { + position: {x: 0.0, y: 0.25, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y and Z axis towards the ray origin so it won't be used, + // but `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to hit test works with local-floor space", + localFloorBasedTestFunctionGenerator(new XRRay(), [pose_3]), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html new file mode 100644 index 0000000000..9f5513fd1e --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +// Creates a test method that leverages regular hit test API (as opposed to hit +// test for transient input). +// |shouldSucceed| - true if the hit test request is expected to succeed, false otherwise +// |endSession| - true if the test case should call session.end() prior to requesting hit test +// |expectedError| - expected error name that should be returned in case shouldSucceed is false +const testFunctionGeneratorRegular = function(shouldSucceed, endSession, expectedError) { + const testFunction = function(session, fakeDeviceController, t) { + session.requestReferenceSpace('viewer').then((viewerRefSpace) => { + + const hitTestOptionsInit = { + space: viewerRefSpace, + offsetRay: new XRRay(), + }; + + if(endSession) { + session.end(); + } + + return session.requestHitTestSource(hitTestOptionsInit).then((hitTestSource) => { + t.step(() => { + assert_true(shouldSucceed, + "`requestHitTestSource` succeeded when it was expected to fail"); + }); + }).catch((error) => { + t.step(() => { + assert_false(shouldSucceed, + "`requestHitTestSource` failed when it was expected to succeed, error: " + error); + assert_equals(error.name, expectedError, + "`requestHitTestSource` failed with unexpected error name"); + }); + }); + }); + }; + + return testFunction; +}; + +xr_session_promise_test("Hit test subscription succeeds if the feature was requested", + testFunctionGeneratorRegular(/*shouldSucceed=*/true, /*endSession=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Hit test subscription fails if the feature was not requested", + testFunctionGeneratorRegular(/*shouldSucceed=*/false, /*endSession=*/false, "NotSupportedError"), + fakeDeviceInitParams, + 'immersive-ar', {}); + +xr_session_promise_test("Hit test subscription fails if the feature was requested but the session already ended", + testFunctionGeneratorRegular(/*shouldSucceed=*/false, /*endSession=*/true, "InvalidStateError"), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html new file mode 100644 index 0000000000..e8a83a62a6 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +// Creates a test method that leverages hit test API for transient input. +// |shouldSucceed| - true if the hit test request is expected to succeed, false otherwise +// |endSession| - true if the test case should call session.end() prior to requesting hit test +// |expectedError| - expected error name that should be returned in case shouldSucceed is false +const testFunctionGeneratorTransient = function(shouldSucceed, endSession, expectedError) { + const testFunction = function(session, fakeDeviceController, t) { + const hitTestOptionsInit = { + profile: "generic-touchscreen", + offsetRay: new XRRay(), + }; + + if(endSession) { + session.end(); + } + + return session.requestHitTestSourceForTransientInput(hitTestOptionsInit) + .then((hitTestSource) => { + t.step(() => { + assert_true(shouldSucceed, + "`requestHitTestSourceForTransientInput` succeeded when it was expected to fail"); + }); + }).catch((error) => { + t.step(() => { + assert_false(shouldSucceed, + "`requestHitTestSourceForTransientInput` failed when it was expected to succeed, error: " + error); + assert_equals(error.name, expectedError, + "`requestHitTestSourceForTransientInput` failed with unexpected error name"); + }); + }); + }; + + return testFunction; +}; + +xr_session_promise_test("Transient hit test subscription succeeds if the feature was requested", + testFunctionGeneratorTransient(/*shouldSucceed=*/true, /*endSession=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Transient hit test subscription fails if the feature was not requested", + testFunctionGeneratorTransient(/*shouldSucceed=*/false, /*endSession=*/false, "NotSupportedError"), + fakeDeviceInitParams, + 'immersive-ar', {}); + +xr_session_promise_test("Transient test subscription fails if the feature was requested but the session already ended", + testFunctionGeneratorTransient(/*shouldSucceed=*/false, /*endSession=*/true, "InvalidStateError"), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html new file mode 100644 index 0000000000..9e0347963c --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, -0.25, 0], + orientation: [0, 0, 0, 1], +}; + +// Start the screen pointer at the same place as the viewer, so it's essentially +// coming straight forward from the middle of the screen. +const SCREEN_POINTER_TRANSFORM = VIEWER_ORIGIN_TRANSFORM; + +const screen_controller_init = { + handedness: "none", + targetRayMode: "screen", + pointerOrigin: SCREEN_POINTER_TRANSFORM, // aka mojo_from_pointer + profiles: ["generic-touchscreen",] +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka floor_from_mojo + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // see webxr_test_constants_fake_world.js for details +}; + +// Generates a test function given the parameters for the transient hit test. +// |ray| - ray that will be used to subscribe to hit test. +// |expectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +// |inputFromPointer| - input from pointer transform that will be used as the input source's +// inputFromPointer (aka pointer origin) in subsequent rAF. +// |nextFrameExpectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +let testFunctionGenerator = function(ray, expectedPoses, inputFromPointer, nextFrameExpectedPoses) { + const testFunction = function(session, fakeDeviceController, t) { + let debug = xr_debug.bind(this, 'testFunction'); + return session.requestReferenceSpace('local').then((localRefSpace) => new Promise((resolve, reject) => { + + const input_source_controller = fakeDeviceController.simulateInputSourceConnection(screen_controller_init); + + requestSkipAnimationFrame(session, (time, frame) => { + debug('rAF 1'); + t.step(() => { + assert_equals(session.inputSources.length, 1); + }); + + const hitTestOptionsInit = { + profile: "generic-touchscreen", + offsetRay: ray, + }; + + session.requestHitTestSourceForTransientInput(hitTestOptionsInit) + .then((hitTestSource) => { + t.step(() => { + assert_not_equals(hitTestSource, null); + }); + + // We got a hit test source, now get the results in subsequent rAFcb: + session.requestAnimationFrame((time, frame) => { + debug('rAF 2'); + const results = frame.getHitTestResultsForTransientInput(hitTestSource); + + t.step(() => { + assert_true(results != null, "Transient input hit tests should not be null"); + assert_equals(results.length, 1, "There should be exactly one group of transient hit test results!"); + assert_equals(results[0].results.length, expectedPoses.length); + for(const [index, expectedPose] of expectedPoses.entries()) { + const pose = results[0].results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "before-move-pose: "); + } + } + }); + + input_source_controller.setPointerOrigin(inputFromPointer, false); + + session.requestAnimationFrame((time, frame) => { + debug('rAF 3'); + const results = frame.getHitTestResultsForTransientInput(hitTestSource); + + t.step(() => { + assert_equals(results[0].results.length, nextFrameExpectedPoses.length); + for(const [index, expectedPose] of nextFrameExpectedPoses.entries()) { + const pose = results[0].results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "after-move-pose: "); + } + } + }); + + debug('resolving'); + resolve(); + }); + }); + }); + }); + })); + }; + + return testFunction; +}; + + +// Pose of the first expected hit test result - straight ahead of the input source, viewer-facing. +const pose_1 = { + position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // Hit test API will set Y axis to the surface normal at the intersection point, + // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. + // If the surface normal and Z axis would be parallel, the hit test API + // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis + // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. + // In this particular case, `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - no move", + testFunctionGenerator(new XRRay(), [pose_1], SCREEN_POINTER_TRANSFORM, [pose_1]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const moved_pointer_transform_1 = { + position: [0, 1, 0], + orientation: [ 0.707, 0, 0, 0.707 ] // 90 degrees around X axis = facing up +}; + +xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - after move - no results", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_1, []), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const pose_2 = { + position: {x: -1.443, y: 1.0, z: -2.5, w: 1.0}, + // Intersection point will be on the same height as the viewer, on the front + // wall. Distance from the front wall to viewer is 2.5m, and we are rotating + // to the left, so X coordinate of the intersection point will be negative + // & equal to -2.5 * tan(30 deg) ~= 1.443m. + orientation: {x: 0.5, y: 0.5, z: 0.5, w: 0.5 }, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y axis facing towards world's + // positive Z axis ([0,0,1]), Z axis to the right ([1,0,0]) and X axis + // towards world's Y axis ([0,1,0]). + // This is equivalent to the rotation around [1, 1, 1] vector by 120 degrees. +}; + +const moved_pointer_transform_2 = { + position: [0, 1, 0], + orientation: [ 0, 0.2588, 0, 0.9659 ] // 30 degrees around Y axis = to the left, + // creating 30-60-90 triangle with the front wall +}; + +xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - after move - 1 result", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_2, [pose_2]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_unlocalizable.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_unlocalizable.https.html new file mode 100644 index 0000000000..d1cb1a5a5c --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_unlocalizable.https.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, 0.25, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka mojo_from_floor + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // webxr_test_constants_fake_world.js has detailed description of the fake world +}; + +// Generates a test function given the parameters for the hit test. It will subscribe +// to a hit test using |refSpaceName| and |ray|, and attempt to obtain poses from returned hit +// test results using a space that is known to be unlocalizable, with the expectation of +// obtaining a null pose for each hit test result. +// |ray| - ray that will be used to subscribe to hit test. +// |refSpaceName| - XRReferenceSpaceType - either 'local', 'local-floor' or 'viewer'. +let testFunctionGenerator = function(ray, refSpaceName) { + const testFunction = function(session, fakeDeviceController, t) { + + const input_source_controller = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: IDENTITY_TRANSFORM, + profiles: [] + }); + + return Promise.all([ + session.requestReferenceSpace('local'), + session.requestReferenceSpace('viewer'), + session.requestReferenceSpace('local-floor'), + ]).then(([localRefSpace, viewerRefSpace, localFloorRefSpace]) => { + + const refSpaceNameToSpace = { + 'local' : localRefSpace, + 'viewer' : viewerRefSpace, + 'local-floor' : localFloorRefSpace + }; + + const hitTestOptionsInit = { + space: refSpaceNameToSpace[refSpaceName], + offsetRay: ray, + }; + + return session.requestHitTestSource(hitTestOptionsInit).then( + (hitTestSource) => new Promise((resolve, reject) => { + + const requestAnimationFrameCallback = function(time, frame) { + + const hitTestResults = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_true(session.inputSources.length > 0, "session.inputSources should not be empty!"); + assert_true(hitTestResults.length > 0, "Results should not be empty!"); + + const input_source = session.inputSources[0]; + + for(const [index, hitTestResult] of hitTestResults.entries()) { + const pose = hitTestResult.getPose(input_source.targetRaySpace); + assert_true(pose == null, "Pose should be null since input source is not localizable"); + } + }); + + resolve(); + }; + + t.step(() => { + assert_true(hitTestSource != null, "Hit test source should not be null"); + }); + + session.requestAnimationFrame(requestAnimationFrameCallback); + })); + }); + }; + + return testFunction; +}; + +// All test cases require local-floor and hit-test. +const sessionInit = { 'requiredFeatures': ['local-floor', 'hit-test'] }; + +xr_session_promise_test( + "Ensures hit test result returns null pose w/unlocalizable space - viewer space", + testFunctionGenerator(new XRRay(), 'viewer'), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/idlharness.https.html b/testing/web-platform/tests/webxr/hit-test/idlharness.https.html new file mode 100644 index 0000000000..f61959b9ed --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/idlharness.https.html @@ -0,0 +1,29 @@ +<!doctype html> +<title>WebXR hit-test IDL tests</title> +<link rel="help" href="https://immersive-web.github.io/hit-test/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> + +<script> + 'use strict'; + + idl_test( + ['webxr-hit-test'], + ['webxr', 'geometry', 'dom'], + async idl_array => { + idl_array.add_objects({ + // TODO: XRHitTestSource + // TODO: XRTransientInputHitTestSource + // TODO: XRHitTestResult + // TODO: XRTransientInputHitTestResult + XRSession: ['xrSession'], + // TODO: XRFrame + XRRay: ['new XRRay()'], + }); + + self.xrSession = await navigator.xr.requestSession("inline"); + } + ); +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/xrRay_constructor.https.html b/testing/web-platform/tests/webxr/hit-test/xrRay_constructor.https.html new file mode 100644 index 0000000000..56b3931341 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/xrRay_constructor.https.html @@ -0,0 +1,159 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script> + +let constructor_test_name = "XRRay constructors work"; + +let constructor_tests = function() { + // Constructor tests for XRRay. + // Spec: https://immersive-web.github.io/webxr/#xrray-interface + + // + // Constructor 1 - from origin and direction + // + + { + // Check defaults - should be 0,0,0,1 for origin and 0,0,-1,0 for direction, + // identity matrix for the transform: + let xrRay1 = new XRRay(); + let xrRay2 = new XRRay({}); + let xrRay3 = new XRRay({}, {}); + + assert_point_approx_equals( + xrRay1.origin, {x : 0.0, y : 0.0, z : 0.0, w : 1.0}, + FLOAT_EPSILON, "origin-default:"); + assert_point_approx_equals( + xrRay1.direction, {x : 0.0, y : 0.0, z : -1.0, w : 0.0}, + FLOAT_EPSILON, "direction-default:"); + assert_matrix_approx_equals( + xrRay1.matrix, IDENTITY_MATRIX, + FLOAT_EPSILON, "matrix-default:"); + + assert_ray_approx_equals(xrRay1, xrRay2, FLOAT_EPSILON, "ray1-ray2-default:"); + assert_ray_approx_equals(xrRay2, xrRay3, FLOAT_EPSILON, "ray2-ray3-default:"); + } + + { + // Check custom value for origin, default for direction: + let originDict = {x : 11.0, y : 12.0, z : 13.0, w : 1.0}; + let xrRay1 = new XRRay(originDict); + let xrRay2 = new XRRay(DOMPoint.fromPoint(originDict)); + let xrRay3 = new XRRay(DOMPointReadOnly.fromPoint(originDict)); + let matrix1 = [ 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 11, 12, 13, 1]; + + assert_point_approx_equals( + xrRay1.origin, originDict, + FLOAT_EPSILON, "origin-custom-direction-default:"); + assert_point_approx_equals( + xrRay1.direction, {x : 0.0, y : 0.0, z : -1.0, w : 0.0}, + FLOAT_EPSILON, "direction-custom-direction-default:"); + assert_matrix_approx_equals( + xrRay1.matrix, matrix1, + FLOAT_EPSILON, "matrix-custom-direction-default:"); + + assert_ray_approx_equals(xrRay1, xrRay2, FLOAT_EPSILON, "ray1-ray2-direction-default:"); + assert_ray_approx_equals(xrRay2, xrRay3, FLOAT_EPSILON, "ray2-ray3-direction-default:"); + } + + { + // Check custom values - ray rotated from -Z onto +X, + // not placed at origin: + // - from DOMPoint + // - from DOMPointReadOnly + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 10.0, y : 0.0, z : 0.0, w : 0.0}; + let directionNorm = {x : 1.0, y : 0.0, z : 0.0, w : 0.0}; + // column-major + let matrix1 = [ 0, 0, 1, 0, + 0, 1, 0, 0, + -1, 0, 0, 0, + 10, 10, 10, 1]; + + let xrRay1 = new XRRay( + originDict, + directionDict); + + let xrRay2 = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let xrRay3 = new XRRay( + DOMPointReadOnly.fromPoint(originDict), + DOMPointReadOnly.fromPoint(directionDict)); + + assert_point_approx_equals( + xrRay1.origin, originDict, + FLOAT_EPSILON, "origin-custom:"); + assert_point_approx_equals( + xrRay1.direction, directionNorm, + FLOAT_EPSILON, "direction-custom:"); + assert_matrix_approx_equals( + xrRay1.matrix, matrix1, + FLOAT_EPSILON, "matrix-custom:"); + + assert_ray_approx_equals(xrRay1, xrRay2, FLOAT_EPSILON, "ray1-ray2:"); + assert_ray_approx_equals(xrRay2, xrRay3, FLOAT_EPSILON, "ray2-ray3:"); + } + + { + // Check that we throw exception on direction too close to 0,0,0: + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 1.0, y : 0.0, z : 0.0, w : 0.0}; + + assert_throws_js(TypeError, () => new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint({x : 0.0, y : 0.0, z : 0.0, w : 0.0}) + ), "Constructor should throw for zero direction"); + + assert_throws_js(TypeError, () => new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint({x : 1.0, y : 0.0, z : 0.0, w : 0.5}) + ), "Constructor should throw for nonzero direction w coordinate"); + + assert_throws_js(TypeError, () => new XRRay( + DOMPoint.fromPoint({x : 10.0, y : 10.0, z : 10.0, w : 0.5}), + DOMPoint.fromPoint(directionDict) + ), "Constructor should throw for non-1 origin w coordinate"); + } + + // + // Constructor 2 - from rigid transform. + // + + { + // Not placed at origin, ray rotated by 135 degrees around Y: + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionQuaternionDict = { x : 0, y : 0.9239, z : 0, w : 0.3827 }; + let directionNorm2 = { x : -0.707, y : 0.0, z : 0.707, w : 0.0 }; + let matrix2 = [-0.707, 0, -0.707, 0, + 0., 1, 0, 0, + 0.707, 0, -0.707, 0, + 10., 10, 10., 1]; + + let xrRay = new XRRay( + new XRRigidTransform( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionQuaternionDict))); + + assert_point_approx_equals( + xrRay.origin, originDict, + FLOAT_EPSILON, "origin-custom-rigid:"); + assert_point_approx_equals( + xrRay.direction, directionNorm2, + FLOAT_EPSILON, "direction-custom-rigid:"); + + assert_matrix_approx_equals( + xrRay.matrix, matrix2, + FLOAT_EPSILON, "matrix-custom-rigid:"); + } +}; + +test(constructor_tests, constructor_test_name); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/xrRay_matrix.https.html b/testing/web-platform/tests/webxr/hit-test/xrRay_matrix.https.html new file mode 100644 index 0000000000..77f134a75f --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/xrRay_matrix.https.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script> + +let matrix_tests_name = "XRRay matrix works"; + +let matrix_tests = function() { + // Matrix tests for XRRay. + // Spec: https://immersive-web.github.io/webxr/#xrray-interface + + const initialOrigin = {x : 0, y : 0, z : 0, w : 1}; + const initialDirection = {x : 0, y : 0, z : -1, w : 0}; + + // Test 1. Simple translation and rotation. + { + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 10.0, y : 0.0, z : 0.0, w : 0.0}; + let directionNorm = {x : 1.0, y : 0.0, z : 0.0, w : 0.0}; + let xrRay = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let transformedOrigin = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialOrigin)); + let transformedDirection = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialDirection)); + + assert_point_approx_equals( + originDict, transformedOrigin, + FLOAT_EPSILON, "origin-test1:"); + assert_point_approx_equals( + directionNorm, transformedDirection, + FLOAT_EPSILON, "direction-test1:"); + } + + // Test 2. Co-linear direction - rotation by 180 deg. + { + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 0.0, y : 0.0, z : 1.0, w : 0.0}; + let directionNorm = {x : 0.0, y : 0.0, z : 1.0, w : 0.0}; + let xrRay = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let transformedOrigin = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialOrigin)); + let transformedDirection = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialDirection)); + + assert_point_approx_equals( + originDict, transformedOrigin, + FLOAT_EPSILON, "origin-test2:"); + assert_point_approx_equals( + directionNorm, transformedDirection, + FLOAT_EPSILON, "direction-test2:"); + } + + // Test 3. No translation. + { + let originDict = {x : 0.0, y : 0.0, z : 0.0, w : 1.0}; + let directionDict = {x : 10.0, y : 0.0, z : 0.0, w : 0.0}; + let directionNorm = {x : 1.0, y : 0.0, z : 0.0, w : 0.0}; + let xrRay = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let transformedOrigin = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialOrigin)); + let transformedDirection = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialDirection)); + + assert_point_approx_equals( + originDict, transformedOrigin, + FLOAT_EPSILON, "origin-test3:"); + assert_point_approx_equals( + directionNorm, transformedDirection, + FLOAT_EPSILON, "direction-test3:"); + } + + // Test 4. No rotation. + { + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 0.0, y : 0.0, z : -1.0, w : 0.0}; + let directionNorm = {x : 0.0, y : 0.0, z : -1.0, w : 0.0}; + let xrRay = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let transformedOrigin = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialOrigin)); + let transformedDirection = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialDirection)); + + assert_point_approx_equals( + originDict, transformedOrigin, + FLOAT_EPSILON, "origin-test4:"); + assert_point_approx_equals( + directionNorm, transformedDirection, + FLOAT_EPSILON, "direction-test4:"); + } +}; + +test(matrix_tests, matrix_tests_name); + +</script> |