<!--
Tests that anchor gets updated as the state of the session changes.
-->
<html>
<head>
<link rel="stylesheet" type="text/css" href="../resources/webxr_e2e.css">
<meta name="timeout" content="long"> <!-- this is a long-running test! -->
</head>
<body>
<canvas id="webgl-canvas"></canvas>
<script src="../../../../../../third_party/blink/web_tests/resources/testharness.js"></script>
<script src="../resources/webxr_e2e.js"></script>
<script>var shouldAutoCreateNonImmersiveSession = false;</script>
<script src="../resources/webxr_boilerplate.js"></script>
<script>
const TestState = Object.freeze({
"Initial": 0,
"HitTestSourceAvailable": 1, // hitTestSource variable is guaranteed to be non-null in this state
"AnchorRequested": 2,
"AnchorAvailable": 3, // createdAnchor variable is guaranteed to be non-null in this state
"AnchorPoseAvailable": 4, // createdAnchor and createdAnchorPose variables are guaranteed to be non-null in this state
"AnchorPoseChanged": 5,
"AnchorPaused": 6,
"AnchorResumed": 7,
"AnchorRemoved": 8,
});
let testState = TestState.Initial;
let hitTestSource = null;
let createdAnchor = null;
let createdAnchorPose = null;
let time_taken_in_ms = null;
// Returns true if q1 and q2 represent the same (or almost the same) orientation.
// This also means that if they only differ by sign, they would still be considered
// equal (since they represent the same orientation, but take a different path).
// |q1|, |q2| - DOMPoints representing quaternions.
function orientation_almost_equal(q1, q2, epsilon) {
const dot = q1.x * q2.x
+ q1.y * q2.y
+ q1.z * q2.z
+ q1.w * q2.w;
const angle = Math.acos(2 * dot * dot -1);
return Math.abs(angle) < epsilon;
};
function position_almost_equal(p1, p2, epsilon) {
const x_diff = p1.x - p2.x;
const y_diff = p1.y - p2.y;
const z_diff = p1.z - p2.z;
const distance = Math.sqrt(x_diff * x_diff + y_diff * y_diff + z_diff * z_diff);
return distance < epsilon;
}
function stepStartHitTesting() {
const start_time = Date.now();
const sessionInfo = sessionInfos[sessionTypes.AR];
const referenceSpace = sessionInfo.currentRefSpace;
console.log('Requesting hit test source');
sessionInfo.currentSession.requestHitTestSource({
space: referenceSpace,
offsetRay: new XRRay()
}).then((hts) => {
console.log('Got hit test source');
// Mark that the hit test source is available.
hitTestSource = hts;
testState = TestState.HitTestSourceAvailable;
}).catch((err) => {
assert_unreached("XRSession.requestHitTestSource() promise rejected.");
});
onARFrameCallback = (session, frame) => {
console.log('Got AR frame, test state is ' + testState);
switch(testState) {
case TestState.Initial: {
// In initial state, there is nothing to do (we're waiting for hit test source).
return;
}
case TestState.HitTestSourceAvailable: {
// Since we already have a hit test source, let's get its results and create an anchor.
const results = frame.getHitTestResults(hitTestSource);
if (results.length) {
const result = results[0];
console.log('Creating anchor');
result.createAnchor().then((anchor) => {
console.log('Anchor created');
createdAnchor = anchor;
testState = TestState.AnchorAvailable;
hitTestSource.cancel();
hitTestSource = null;
}).catch((err) => {
// Fail the test.
assert_unreached("XRHitTestResult.createAnchor() promise rejected.");
});
// Mark that we are waiting for anchor creation.
testState = TestState.AnchorRequested;
}
return;
}
case TestState.AnchorAvailable: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
const anchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
if (anchorPose) {
testState = TestState.AnchorPoseAvailable;
createdAnchorPose = anchorPose;
}
return;
}
case TestState.AnchorPoseAvailable: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
if (currentAnchorPose) {
// We'll only leave this state if anchor pose is significantly different than previous anchor pose.
const previousAnchorPosition = createdAnchorPose.transform.position;
const currentAnchorPosition = currentAnchorPose.transform.position;
const previousAnchorOrientation = createdAnchorPose.transform.orientation;
const currentAnchorOrientation = currentAnchorPose.transform.orientation;
if (position_almost_equal(previousAnchorPosition, currentAnchorPosition, 0.05)
&& orientation_almost_equal(previousAnchorOrientation, currentAnchorOrientation, 5 * (Math.PI / 180))) {
// Anchor pose is still the same, keep waiting.
} else {
testState = TestState.AnchorPoseChanged;
}
}
return;
}
case TestState.AnchorPoseChanged: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
// Now let's wait for the anchor tracking to become paused:
const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
if(!currentAnchorPose) {
testState = TestState.AnchorPaused;
}
return;
}
case TestState.AnchorPaused: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
// Now wait for the tracking to be resumed:
const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
if(currentAnchorPose) {
testState = TestState.AnchorResumed;
}
return;
}
case TestState.AnchorResumed: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
createdAnchor.delete();
assert_throws_dom('InvalidStateError', () => {
createdAnchor.anchorSpace;
});
testState = TestState.AnchorRemoved;
return;
}
case TestState.AnchorRemoved:
assert_true(!frame.trackedAnchors.has(createdAnchor), "Created anchor should not be tracked after it got removed!");
time_taken_in_ms = Date.now() - start_time;
done();
return;
default:
return;
}
};
}
</script>
</body>
</html>