2441 lines
80 KiB
JavaScript
2441 lines
80 KiB
JavaScript
import * as vrMojom from '/gen/device/vr/public/mojom/vr_service.mojom.m.js';
|
|
import * as xrSessionMojom from '/gen/device/vr/public/mojom/xr_session.mojom.m.js';
|
|
import {GamepadHand, GamepadMapping} from '/gen/device/gamepad/public/mojom/gamepad.mojom.m.js';
|
|
|
|
// This polyfill library implements the WebXR Test API as specified here:
|
|
// https://github.com/immersive-web/webxr-test-api
|
|
|
|
const defaultMojoFromStage = {
|
|
matrix: [1, 0, 0, 0,
|
|
0, 1, 0, 0,
|
|
0, 0, 1, 0,
|
|
0, -1.65, 0, 1]
|
|
};
|
|
const default_stage_parameters = {
|
|
mojoFromStage: defaultMojoFromStage,
|
|
bounds: null
|
|
};
|
|
|
|
const default_framebuffer_scale = 0.7;
|
|
|
|
function getMatrixFromTransform(transform) {
|
|
const x = transform.orientation[0];
|
|
const y = transform.orientation[1];
|
|
const z = transform.orientation[2];
|
|
const w = transform.orientation[3];
|
|
|
|
const m11 = 1.0 - 2.0 * (y * y + z * z);
|
|
const m21 = 2.0 * (x * y + z * w);
|
|
const m31 = 2.0 * (x * z - y * w);
|
|
|
|
const m12 = 2.0 * (x * y - z * w);
|
|
const m22 = 1.0 - 2.0 * (x * x + z * z);
|
|
const m32 = 2.0 * (y * z + x * w);
|
|
|
|
const m13 = 2.0 * (x * z + y * w);
|
|
const m23 = 2.0 * (y * z - x * w);
|
|
const m33 = 1.0 - 2.0 * (x * x + y * y);
|
|
|
|
const m14 = transform.position[0];
|
|
const m24 = transform.position[1];
|
|
const m34 = transform.position[2];
|
|
|
|
// Column-major linearized order is expected.
|
|
return [m11, m21, m31, 0,
|
|
m12, m22, m32, 0,
|
|
m13, m23, m33, 0,
|
|
m14, m24, m34, 1];
|
|
}
|
|
|
|
function getPoseFromTransform(transform) {
|
|
const [px, py, pz] = transform.position;
|
|
const [ox, oy, oz, ow] = transform.orientation;
|
|
return {
|
|
position: {x: px, y: py, z: pz},
|
|
orientation: {x: ox, y: oy, z: oz, w: ow},
|
|
};
|
|
}
|
|
|
|
function composeGFXTransform(fakeTransformInit) {
|
|
return {matrix: getMatrixFromTransform(fakeTransformInit)};
|
|
}
|
|
|
|
// Value equality for camera image init objects - they must contain `width` &
|
|
// `height` properties and may contain `pixels` property.
|
|
function isSameCameraImageInit(rhs, lhs) {
|
|
return lhs.width === rhs.width && lhs.height === rhs.height && lhs.pixels === rhs.pixels;
|
|
}
|
|
|
|
class ChromeXRTest {
|
|
constructor() {
|
|
this.mockVRService_ = new MockVRService();
|
|
}
|
|
|
|
// WebXR Test API
|
|
simulateDeviceConnection(init_params) {
|
|
return Promise.resolve(this.mockVRService_._addRuntime(init_params));
|
|
}
|
|
|
|
disconnectAllDevices() {
|
|
this.mockVRService_._removeAllRuntimes();
|
|
return Promise.resolve();
|
|
}
|
|
|
|
simulateUserActivation(callback) {
|
|
if (window.top !== window) {
|
|
// test_driver.click only works for the toplevel frame. This alternate
|
|
// Chrome-specific method is sufficient for starting an XR session in an
|
|
// iframe, and is used in platform-specific tests.
|
|
//
|
|
// TODO(https://github.com/web-platform-tests/wpt/issues/20282): use
|
|
// a cross-platform method if available.
|
|
xr_debug('simulateUserActivation', 'use eventSender');
|
|
document.addEventListener('click', callback);
|
|
eventSender.mouseMoveTo(0, 0);
|
|
eventSender.mouseDown();
|
|
eventSender.mouseUp();
|
|
document.removeEventListener('click', callback);
|
|
return;
|
|
}
|
|
const button = document.createElement('button');
|
|
button.textContent = 'click to continue test';
|
|
button.style.display = 'block';
|
|
button.style.fontSize = '20px';
|
|
button.style.padding = '10px';
|
|
button.onclick = () => {
|
|
callback();
|
|
document.body.removeChild(button);
|
|
};
|
|
document.body.appendChild(button);
|
|
test_driver.click(button);
|
|
}
|
|
|
|
// Helper method leveraged by chrome-specific setups.
|
|
Debug(name, msg) {
|
|
console.log(new Date().toISOString() + ' DEBUG[' + name + '] ' + msg);
|
|
}
|
|
}
|
|
|
|
// Mocking class definitions
|
|
|
|
// Mock service implements the VRService mojo interface.
|
|
class MockVRService {
|
|
constructor() {
|
|
this.receiver_ = new vrMojom.VRServiceReceiver(this);
|
|
this.runtimes_ = [];
|
|
|
|
this.interceptor_ =
|
|
new MojoInterfaceInterceptor(vrMojom.VRService.$interfaceName);
|
|
this.interceptor_.oninterfacerequest =
|
|
e => this.receiver_.$.bindHandle(e.handle);
|
|
this.interceptor_.start();
|
|
}
|
|
|
|
// WebXR Test API Implementation Helpers
|
|
_addRuntime(fakeDeviceInit) {
|
|
const runtime = new MockRuntime(fakeDeviceInit, this);
|
|
this.runtimes_.push(runtime);
|
|
|
|
if (this.client_) {
|
|
this.client_.onDeviceChanged();
|
|
}
|
|
|
|
return runtime;
|
|
}
|
|
|
|
_removeAllRuntimes() {
|
|
if (this.client_) {
|
|
this.client_.onDeviceChanged();
|
|
}
|
|
|
|
this.runtimes_ = [];
|
|
}
|
|
|
|
_removeRuntime(device) {
|
|
const index = this.runtimes_.indexOf(device);
|
|
if (index >= 0) {
|
|
this.runtimes_.splice(index, 1);
|
|
if (this.client_) {
|
|
this.client_.onDeviceChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
// VRService overrides
|
|
setClient(client) {
|
|
if (this.client_) {
|
|
throw new Error("setClient should only be called once");
|
|
}
|
|
|
|
this.client_ = client;
|
|
}
|
|
|
|
requestSession(sessionOptions) {
|
|
const requests = [];
|
|
// Request a session from all the runtimes.
|
|
for (let i = 0; i < this.runtimes_.length; i++) {
|
|
requests[i] = this.runtimes_[i]._requestRuntimeSession(sessionOptions);
|
|
}
|
|
|
|
return Promise.all(requests).then((results) => {
|
|
// Find and return the first successful result.
|
|
for (let i = 0; i < results.length; i++) {
|
|
if (results[i].session) {
|
|
// Construct a dummy metrics recorder
|
|
const metricsRecorderPtr = new vrMojom.XRSessionMetricsRecorderRemote();
|
|
metricsRecorderPtr.$.bindNewPipeAndPassReceiver().handle.close();
|
|
|
|
const success = {
|
|
session: results[i].session,
|
|
metricsRecorder: metricsRecorderPtr,
|
|
};
|
|
|
|
return {result: {success}};
|
|
}
|
|
}
|
|
|
|
// If there were no successful results, returns a null session.
|
|
return {
|
|
result: {failureReason: xrSessionMojom.RequestSessionError.NO_RUNTIME_FOUND}
|
|
};
|
|
});
|
|
}
|
|
|
|
supportsSession(sessionOptions) {
|
|
const requests = [];
|
|
// Check supports on all the runtimes.
|
|
for (let i = 0; i < this.runtimes_.length; i++) {
|
|
requests[i] = this.runtimes_[i]._runtimeSupportsSession(sessionOptions);
|
|
}
|
|
|
|
return Promise.all(requests).then((results) => {
|
|
// Find and return the first successful result.
|
|
for (let i = 0; i < results.length; i++) {
|
|
if (results[i].supportsSession) {
|
|
return results[i];
|
|
}
|
|
}
|
|
|
|
// If there were no successful results, returns false.
|
|
return {supportsSession: false};
|
|
});
|
|
}
|
|
|
|
exitPresent() {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
setFramesThrottled(throttled) {
|
|
this.setFramesThrottledImpl(throttled);
|
|
}
|
|
|
|
// We cannot override the mojom interceptors via the prototype; so this method
|
|
// and the above indirection exist to allow overrides by internal code.
|
|
setFramesThrottledImpl(throttled) {}
|
|
|
|
// Only handles asynchronous calls to makeXrCompatible. Synchronous calls are
|
|
// not supported in Javascript.
|
|
makeXrCompatible() {
|
|
if (this.runtimes_.length == 0) {
|
|
return {
|
|
xrCompatibleResult: vrMojom.XrCompatibleResult.kNoDeviceAvailable
|
|
};
|
|
}
|
|
return {xrCompatibleResult: vrMojom.XrCompatibleResult.kAlreadyCompatible};
|
|
}
|
|
}
|
|
|
|
class FakeXRAnchorController {
|
|
constructor() {
|
|
// Private properties.
|
|
this.device_ = null;
|
|
this.id_ = null;
|
|
this.dirty_ = true;
|
|
|
|
// Properties backing up public attributes / methods.
|
|
this.deleted_ = false;
|
|
this.paused_ = false;
|
|
this.anchorOrigin_ = XRMathHelper.identity();
|
|
}
|
|
|
|
// WebXR Test API (Anchors Extension)
|
|
get deleted() {
|
|
return this.deleted_;
|
|
}
|
|
|
|
pauseTracking() {
|
|
if(!this.paused_) {
|
|
this.paused_ = true;
|
|
this.dirty_ = true;
|
|
}
|
|
}
|
|
|
|
resumeTracking() {
|
|
if(this.paused_) {
|
|
this.paused_ = false;
|
|
this.dirty_ = true;
|
|
}
|
|
}
|
|
|
|
stopTracking() {
|
|
if(!this.deleted_) {
|
|
this.device_._deleteAnchorController(this.id_);
|
|
|
|
this.deleted_ = true;
|
|
this.dirty_ = true;
|
|
}
|
|
}
|
|
|
|
setAnchorOrigin(anchorOrigin) {
|
|
this.anchorOrigin_ = getMatrixFromTransform(anchorOrigin);
|
|
this.dirty_ = true;
|
|
}
|
|
|
|
// Internal implementation:
|
|
set id(value) {
|
|
this.id_ = value;
|
|
}
|
|
|
|
set device(value) {
|
|
this.device_ = value;
|
|
}
|
|
|
|
get dirty() {
|
|
return this.dirty_;
|
|
}
|
|
|
|
get paused() {
|
|
return this.paused_;
|
|
}
|
|
|
|
_markProcessed() {
|
|
this.dirty_ = false;
|
|
}
|
|
|
|
_getAnchorOrigin() {
|
|
return this.anchorOrigin_;
|
|
}
|
|
}
|
|
|
|
// Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock
|
|
// for XRPresentationProvider. Implements FakeXRDevice test API.
|
|
class MockRuntime {
|
|
// Mapping from string feature names to the corresponding mojo types.
|
|
// This is exposed as a member for extensibility.
|
|
static _featureToMojoMap = {
|
|
'viewer': xrSessionMojom.XRSessionFeature.REF_SPACE_VIEWER,
|
|
'local': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL,
|
|
'local-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL_FLOOR,
|
|
'bounded-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_BOUNDED_FLOOR,
|
|
'unbounded': xrSessionMojom.XRSessionFeature.REF_SPACE_UNBOUNDED,
|
|
'hit-test': xrSessionMojom.XRSessionFeature.HIT_TEST,
|
|
'dom-overlay': xrSessionMojom.XRSessionFeature.DOM_OVERLAY,
|
|
'light-estimation': xrSessionMojom.XRSessionFeature.LIGHT_ESTIMATION,
|
|
'anchors': xrSessionMojom.XRSessionFeature.ANCHORS,
|
|
'depth-sensing': xrSessionMojom.XRSessionFeature.DEPTH,
|
|
'secondary-views': xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS,
|
|
'camera-access': xrSessionMojom.XRSessionFeature.CAMERA_ACCESS,
|
|
'layers': xrSessionMojom.XRSessionFeature.LAYERS,
|
|
};
|
|
|
|
static _sessionModeToMojoMap = {
|
|
"inline": xrSessionMojom.XRSessionMode.kInline,
|
|
"immersive-vr": xrSessionMojom.XRSessionMode.kImmersiveVr,
|
|
"immersive-ar": xrSessionMojom.XRSessionMode.kImmersiveAr,
|
|
};
|
|
|
|
static _environmentBlendModeToMojoMap = {
|
|
"opaque": vrMojom.XREnvironmentBlendMode.kOpaque,
|
|
"alpha-blend": vrMojom.XREnvironmentBlendMode.kAlphaBlend,
|
|
"additive": vrMojom.XREnvironmentBlendMode.kAdditive,
|
|
};
|
|
|
|
static _interactionModeToMojoMap = {
|
|
"screen-space": vrMojom.XRInteractionMode.kScreenSpace,
|
|
"world-space": vrMojom.XRInteractionMode.kWorldSpace,
|
|
};
|
|
|
|
static _depthTypeToMojoMap = {
|
|
"raw": xrSessionMojom.XRDepthType.kRawDepth,
|
|
"smooth": xrSessionMojom.XRDepthType.kSmoothDepth,
|
|
};
|
|
|
|
static _depthUsageToMojoMap = {
|
|
"cpu-optimized": xrSessionMojom.XRDepthUsage.kCPUOptimized,
|
|
"gpu-optimized": xrSessionMojom.XRDepthUsage.kGPUOptimized,
|
|
};
|
|
|
|
static _depthDataFormatToMojoMap = {
|
|
"luminance-alpha": xrSessionMojom.XRDepthDataFormat.kLuminanceAlpha,
|
|
"float32": xrSessionMojom.XRDepthDataFormat.kFloat32,
|
|
"unsigned-short": xrSessionMojom.XRDepthDataFormat.kUnsignedShort,
|
|
};
|
|
|
|
|
|
constructor(fakeDeviceInit, service) {
|
|
this.sessionClient_ = null;
|
|
this.presentation_provider_ = new MockXRPresentationProvider();
|
|
|
|
this.pose_ = null;
|
|
this.next_frame_id_ = 0;
|
|
this.bounds_ = null;
|
|
this.send_mojo_space_reset_ = false;
|
|
this.stageParameters_ = null;
|
|
this.stageParametersId_ = 1;
|
|
|
|
this.service_ = service;
|
|
|
|
this.framesOfReference = {};
|
|
|
|
this.input_sources_ = new Map();
|
|
this.next_input_source_index_ = 1;
|
|
|
|
// Currently active hit test subscriptons.
|
|
this.hitTestSubscriptions_ = new Map();
|
|
// Currently active transient hit test subscriptions.
|
|
this.transientHitTestSubscriptions_ = new Map();
|
|
// ID of the next subscription to be assigned.
|
|
this.next_hit_test_id_ = 1n;
|
|
|
|
this.anchor_controllers_ = new Map();
|
|
// ID of the next anchor to be assigned.
|
|
this.next_anchor_id_ = 1n;
|
|
// Anchor creation callback (initially null, can be set by tests).
|
|
this.anchor_creation_callback_ = null;
|
|
|
|
this.depthSensingData_ = null;
|
|
this.depthSensingDataDirty_ = false;
|
|
|
|
let supportedModes = [];
|
|
if (fakeDeviceInit.supportedModes) {
|
|
supportedModes = fakeDeviceInit.supportedModes.slice();
|
|
if (fakeDeviceInit.supportedModes.length === 0) {
|
|
supportedModes = ["inline"];
|
|
}
|
|
} else {
|
|
// Back-compat mode.
|
|
console.warn("Please use `supportedModes` to signal which modes are supported by this device.");
|
|
if (fakeDeviceInit.supportsImmersive == null) {
|
|
throw new TypeError("'supportsImmersive' must be set");
|
|
}
|
|
|
|
supportedModes = ["inline"];
|
|
if (fakeDeviceInit.supportsImmersive) {
|
|
supportedModes.push("immersive-vr");
|
|
}
|
|
}
|
|
|
|
this.supportedModes_ = this._convertModesToEnum(supportedModes);
|
|
if (this.supportedModes_.length == 0) {
|
|
console.error("Device has empty supported modes array!");
|
|
throw new InvalidStateError();
|
|
}
|
|
|
|
if (fakeDeviceInit.viewerOrigin != null) {
|
|
this.setViewerOrigin(fakeDeviceInit.viewerOrigin);
|
|
}
|
|
|
|
if (fakeDeviceInit.floorOrigin != null) {
|
|
this.setFloorOrigin(fakeDeviceInit.floorOrigin);
|
|
}
|
|
|
|
if (fakeDeviceInit.world) {
|
|
this.setWorld(fakeDeviceInit.world);
|
|
}
|
|
|
|
if (fakeDeviceInit.depthSensingData) {
|
|
this.setDepthSensingData(fakeDeviceInit.depthSensingData);
|
|
}
|
|
|
|
this.defaultFramebufferScale_ = default_framebuffer_scale;
|
|
this.enviromentBlendMode_ = this._convertBlendModeToEnum(fakeDeviceInit.environmentBlendMode);
|
|
this.interactionMode_ = this._convertInteractionModeToEnum(fakeDeviceInit.interactionMode);
|
|
|
|
// This appropriately handles if the coordinates are null
|
|
this.setBoundsGeometry(fakeDeviceInit.boundsCoordinates);
|
|
|
|
this.setViews(fakeDeviceInit.views, fakeDeviceInit.secondaryViews);
|
|
|
|
this._setDepthSupport(fakeDeviceInit.depthSupport || {});
|
|
|
|
// Need to support webVR which doesn't have a notion of features
|
|
this._setFeatures(fakeDeviceInit.supportedFeatures || []);
|
|
}
|
|
|
|
// WebXR Test API
|
|
setViews(primaryViews, secondaryViews) {
|
|
this.cameraImage_ = null;
|
|
this.primaryViews_ = [];
|
|
this.secondaryViews_ = [];
|
|
let xOffset = 0;
|
|
if (primaryViews) {
|
|
this.primaryViews_ = [];
|
|
xOffset = this._setViews(primaryViews, xOffset, this.primaryViews_);
|
|
const cameraImage = this._findCameraImage(primaryViews);
|
|
|
|
if (cameraImage) {
|
|
this.cameraImage_ = cameraImage;
|
|
}
|
|
}
|
|
|
|
if (secondaryViews) {
|
|
this.secondaryViews_ = [];
|
|
this._setViews(secondaryViews, xOffset, this.secondaryViews_);
|
|
const cameraImage = this._findCameraImage(secondaryViews);
|
|
|
|
if (cameraImage) {
|
|
if (!isSameCameraImageInit(this.cameraImage_, cameraImage)) {
|
|
throw new Error("If present, camera resolutions on each view must match each other!"
|
|
+ " Secondary views' camera doesn't match primary views.");
|
|
}
|
|
|
|
this.cameraImage_ = cameraImage;
|
|
}
|
|
}
|
|
}
|
|
|
|
disconnect() {
|
|
this.service_._removeRuntime(this);
|
|
this.presentation_provider_._close();
|
|
if (this.sessionClient_) {
|
|
this.sessionClient_.$.close();
|
|
this.sessionClient_ = null;
|
|
}
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
setViewerOrigin(origin, emulatedPosition = false) {
|
|
const p = origin.position;
|
|
const q = origin.orientation;
|
|
this.pose_ = {
|
|
orientation: { x: q[0], y: q[1], z: q[2], w: q[3] },
|
|
position: { x: p[0], y: p[1], z: p[2] },
|
|
emulatedPosition: emulatedPosition,
|
|
angularVelocity: null,
|
|
linearVelocity: null,
|
|
angularAcceleration: null,
|
|
linearAcceleration: null,
|
|
inputState: null,
|
|
poseIndex: 0
|
|
};
|
|
}
|
|
|
|
clearViewerOrigin() {
|
|
this.pose_ = null;
|
|
}
|
|
|
|
setFloorOrigin(floorOrigin) {
|
|
if (!this.stageParameters_) {
|
|
this.stageParameters_ = default_stage_parameters;
|
|
this.stageParameters_.bounds = this.bounds_;
|
|
}
|
|
|
|
// floorOrigin is passed in as mojoFromStage.
|
|
this.stageParameters_.mojoFromStage =
|
|
{matrix: getMatrixFromTransform(floorOrigin)};
|
|
|
|
this._onStageParametersUpdated();
|
|
}
|
|
|
|
clearFloorOrigin() {
|
|
if (this.stageParameters_) {
|
|
this.stageParameters_ = null;
|
|
this._onStageParametersUpdated();
|
|
}
|
|
}
|
|
|
|
setBoundsGeometry(bounds) {
|
|
if (bounds == null) {
|
|
this.bounds_ = null;
|
|
} else if (bounds.length < 3) {
|
|
throw new Error("Bounds must have a length of at least 3");
|
|
} else {
|
|
this.bounds_ = bounds;
|
|
}
|
|
|
|
// We can only set bounds if we have stageParameters set; otherwise, we
|
|
// don't know the transform from local space to bounds space.
|
|
// We'll cache the bounds so that they can be set in the future if the
|
|
// floorLevel transform is set, but we won't update them just yet.
|
|
if (this.stageParameters_) {
|
|
this.stageParameters_.bounds = this.bounds_;
|
|
this._onStageParametersUpdated();
|
|
}
|
|
}
|
|
|
|
simulateResetPose() {
|
|
this.send_mojo_space_reset_ = true;
|
|
}
|
|
|
|
simulateVisibilityChange(visibilityState) {
|
|
let mojoState = null;
|
|
switch (visibilityState) {
|
|
case "visible":
|
|
mojoState = vrMojom.XRVisibilityState.VISIBLE;
|
|
break;
|
|
case "visible-blurred":
|
|
mojoState = vrMojom.XRVisibilityState.VISIBLE_BLURRED;
|
|
break;
|
|
case "hidden":
|
|
mojoState = vrMojom.XRVisibilityState.HIDDEN;
|
|
break;
|
|
}
|
|
if (mojoState && this.sessionClient_) {
|
|
this.sessionClient_.onVisibilityStateChanged(mojoState);
|
|
}
|
|
}
|
|
|
|
simulateInputSourceConnection(fakeInputSourceInit) {
|
|
const index = this.next_input_source_index_;
|
|
this.next_input_source_index_++;
|
|
|
|
const source = new MockXRInputSource(fakeInputSourceInit, index, this);
|
|
this.input_sources_.set(index, source);
|
|
return source;
|
|
}
|
|
|
|
// WebXR Test API Hit Test extensions
|
|
setWorld(world) {
|
|
this.world_ = world;
|
|
}
|
|
|
|
clearWorld() {
|
|
this.world_ = null;
|
|
}
|
|
|
|
// WebXR Test API Anchor extensions
|
|
setAnchorCreationCallback(callback) {
|
|
this.anchor_creation_callback_ = callback;
|
|
}
|
|
|
|
setHitTestSourceCreationCallback(callback) {
|
|
this.hit_test_source_creation_callback_ = callback;
|
|
}
|
|
|
|
// WebXR Test API Lighting estimation extensions
|
|
setLightEstimate(fakeXrLightEstimateInit) {
|
|
if (!fakeXrLightEstimateInit.sphericalHarmonicsCoefficients) {
|
|
throw new TypeError("sphericalHarmonicsCoefficients must be set");
|
|
}
|
|
|
|
if (fakeXrLightEstimateInit.sphericalHarmonicsCoefficients.length != 27) {
|
|
throw new TypeError("Must supply all 27 sphericalHarmonicsCoefficients");
|
|
}
|
|
|
|
if (fakeXrLightEstimateInit.primaryLightDirection && fakeXrLightEstimateInit.primaryLightDirection.w != 0) {
|
|
throw new TypeError("W component of primaryLightDirection must be 0");
|
|
}
|
|
|
|
if (fakeXrLightEstimateInit.primaryLightIntensity && fakeXrLightEstimateInit.primaryLightIntensity.w != 1) {
|
|
throw new TypeError("W component of primaryLightIntensity must be 1");
|
|
}
|
|
|
|
// If the primaryLightDirection or primaryLightIntensity aren't set, we need to set them
|
|
// to the defaults that the spec expects. ArCore will either give us everything or nothing,
|
|
// so these aren't nullable on the mojom.
|
|
if (!fakeXrLightEstimateInit.primaryLightDirection) {
|
|
fakeXrLightEstimateInit.primaryLightDirection = { x: 0.0, y: 1.0, z: 0.0, w: 0.0 };
|
|
}
|
|
|
|
if (!fakeXrLightEstimateInit.primaryLightIntensity) {
|
|
fakeXrLightEstimateInit.primaryLightIntensity = { x: 0.0, y: 0.0, z: 0.0, w: 1.0 };
|
|
}
|
|
|
|
let c = fakeXrLightEstimateInit.sphericalHarmonicsCoefficients;
|
|
|
|
this.light_estimate_ = {
|
|
lightProbe: {
|
|
// XRSphereicalHarmonics
|
|
sphericalHarmonics: {
|
|
coefficients: [
|
|
{ red: c[0], green: c[1], blue: c[2] },
|
|
{ red: c[3], green: c[4], blue: c[5] },
|
|
{ red: c[6], green: c[7], blue: c[8] },
|
|
{ red: c[9], green: c[10], blue: c[11] },
|
|
{ red: c[12], green: c[13], blue: c[14] },
|
|
{ red: c[15], green: c[16], blue: c[17] },
|
|
{ red: c[18], green: c[19], blue: c[20] },
|
|
{ red: c[21], green: c[22], blue: c[23] },
|
|
{ red: c[24], green: c[25], blue: c[26] }
|
|
]
|
|
},
|
|
// Vector3dF
|
|
mainLightDirection: {
|
|
x: fakeXrLightEstimateInit.primaryLightDirection.x,
|
|
y: fakeXrLightEstimateInit.primaryLightDirection.y,
|
|
z: fakeXrLightEstimateInit.primaryLightDirection.z
|
|
},
|
|
// RgbTupleF32
|
|
mainLightIntensity: {
|
|
red: fakeXrLightEstimateInit.primaryLightIntensity.x,
|
|
green: fakeXrLightEstimateInit.primaryLightIntensity.y,
|
|
blue: fakeXrLightEstimateInit.primaryLightIntensity.z
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WebXR Test API depth Sensing Extensions
|
|
_setDepthSupport(depthSupport) {
|
|
this.depthSupport_ = {};
|
|
|
|
this.depthSupport_.depthTypes = [];
|
|
for (const type of (depthSupport.depthTypes || [])) {
|
|
this.depthSupport_.depthTypes.push(MockRuntime._depthTypeToMojoMap[type]);
|
|
}
|
|
|
|
this.depthSupport_.depthFormats = [];
|
|
for (const format of (depthSupport.depthFormats || [])) {
|
|
this.depthSupport_.depthFormats.push(MockRuntime._depthDataFormatToMojoMap[format]);
|
|
}
|
|
|
|
this.depthSupport_.depthUsages = [];
|
|
for (const usage of (depthSupport.depthUsages || [])) {
|
|
// Because chrome doesn't support gpu-optimized for any devices at present
|
|
// avoid "false positive" WPTs by indicating that we don't support
|
|
// gpu-optimized.
|
|
if (usage === "gpu-optimized") {
|
|
continue;
|
|
}
|
|
|
|
this.depthSupport_.depthUsages.push(MockRuntime._depthUsageToMojoMap[usage]);
|
|
}
|
|
}
|
|
|
|
setDepthSensingData(depthSensingData) {
|
|
for(const key of ["depthData", "normDepthBufferFromNormView", "rawValueToMeters", "width", "height"]) {
|
|
if(!(key in depthSensingData)) {
|
|
throw new TypeError("Required key not present. Key: " + key);
|
|
}
|
|
}
|
|
|
|
if(depthSensingData.depthData != null) {
|
|
// Create new object w/ properties based on the depthSensingData, but
|
|
// convert the FakeXRRigidTransformInit into a transformation matrix object.
|
|
this.depthSensingData_ = Object.assign({},
|
|
depthSensingData, {
|
|
normDepthBufferFromNormView: composeGFXTransform(depthSensingData.normDepthBufferFromNormView),
|
|
});
|
|
} else {
|
|
throw new TypeError("`depthData` is not set");
|
|
}
|
|
|
|
this.depthSensingDataDirty_ = true;
|
|
}
|
|
|
|
clearDepthSensingData() {
|
|
this.depthSensingData_ = null;
|
|
this.depthSensingDataDirty_ = true;
|
|
}
|
|
|
|
// Internal Implementation/Helper Methods
|
|
_convertModeToEnum(sessionMode) {
|
|
if (sessionMode in MockRuntime._sessionModeToMojoMap) {
|
|
return MockRuntime._sessionModeToMojoMap[sessionMode];
|
|
}
|
|
|
|
throw new TypeError("Unrecognized value for XRSessionMode enum: " + sessionMode);
|
|
}
|
|
|
|
_convertModesToEnum(sessionModes) {
|
|
return sessionModes.map(mode => this._convertModeToEnum(mode));
|
|
}
|
|
|
|
_convertBlendModeToEnum(blendMode) {
|
|
if (blendMode in MockRuntime._environmentBlendModeToMojoMap) {
|
|
return MockRuntime._environmentBlendModeToMojoMap[blendMode];
|
|
} else {
|
|
if (this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
|
|
return vrMojom.XREnvironmentBlendMode.kAdditive;
|
|
} else if (this.supportedModes_.includes(
|
|
xrSessionMojom.XRSessionMode.kImmersiveVr)) {
|
|
return vrMojom.XREnvironmentBlendMode.kOpaque;
|
|
}
|
|
}
|
|
}
|
|
|
|
_convertInteractionModeToEnum(interactionMode) {
|
|
if (interactionMode in MockRuntime._interactionModeToMojoMap) {
|
|
return MockRuntime._interactionModeToMojoMap[interactionMode];
|
|
} else {
|
|
return vrMojom.XRInteractionMode.kWorldSpace;
|
|
}
|
|
}
|
|
|
|
_setViews(deviceViews, xOffset, views) {
|
|
for (let i = 0; i < deviceViews.length; i++) {
|
|
views[i] = this._getView(deviceViews[i], xOffset);
|
|
xOffset += deviceViews[i].resolution.width;
|
|
}
|
|
|
|
return xOffset;
|
|
}
|
|
|
|
_findCameraImage(views) {
|
|
const viewWithCamera = views.find(view => view.cameraImageInit);
|
|
if (viewWithCamera) {
|
|
//If we have one view with a camera resolution, all views should have the same camera resolution.
|
|
const allViewsHaveSameCamera = views.every(
|
|
view => isSameCameraImageInit(view.cameraImageInit, viewWithCamera.cameraImageInit));
|
|
|
|
if (!allViewsHaveSameCamera) {
|
|
throw new Error("If present, camera resolutions on each view must match each other!");
|
|
}
|
|
|
|
return viewWithCamera.cameraImageInit;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_onStageParametersUpdated() {
|
|
// Indicate for the frame loop that the stage parameters have been updated.
|
|
this.stageParametersId_++;
|
|
}
|
|
|
|
_getDefaultViews() {
|
|
if (this.primaryViews_) {
|
|
return this.primaryViews_;
|
|
}
|
|
|
|
const viewport_size = 20;
|
|
return [{
|
|
eye: vrMojom.XREye.kLeft,
|
|
geometry: {
|
|
fieldOfView: {
|
|
upDegrees: 48.316,
|
|
downDegrees: 50.099,
|
|
leftDegrees: 50.899,
|
|
rightDegrees: 35.197
|
|
},
|
|
mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
|
|
position: [-0.032, 0, 0],
|
|
orientation: [0, 0, 0, 1]
|
|
}))
|
|
},
|
|
viewport: { x: 0, y: 0, width: viewport_size, height: viewport_size }
|
|
},
|
|
{
|
|
eye: vrMojom.XREye.kRight,
|
|
geometry: {
|
|
fieldOfView: {
|
|
upDegrees: 48.316,
|
|
downDegrees: 50.099,
|
|
leftDegrees: 50.899,
|
|
rightDegrees: 35.197
|
|
},
|
|
mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
|
|
position: [0.032, 0, 0],
|
|
orientation: [0, 0, 0, 1]
|
|
}))
|
|
},
|
|
viewport: { x: viewport_size, y: 0, width: viewport_size, height: viewport_size }
|
|
}];
|
|
}
|
|
|
|
_getFovFromProjectionMatrix(projectionMatrix) {
|
|
const m = projectionMatrix;
|
|
|
|
function toDegrees(tan) {
|
|
return Math.atan(tan) * 180 / Math.PI;
|
|
}
|
|
|
|
const leftTan = (1 - m[8]) / m[0];
|
|
const rightTan = (1 + m[8]) / m[0];
|
|
const upTan = (1 + m[9]) / m[5];
|
|
const downTan = (1 - m[9]) / m[5];
|
|
|
|
return {
|
|
upDegrees: toDegrees(upTan),
|
|
downDegrees: toDegrees(downTan),
|
|
leftDegrees: toDegrees(leftTan),
|
|
rightDegrees: toDegrees(rightTan)
|
|
};
|
|
}
|
|
|
|
// This function converts between the matrix provided by the WebXR test API
|
|
// and the internal data representation.
|
|
_getView(fakeXRViewInit, xOffset) {
|
|
let fov = null;
|
|
|
|
if (fakeXRViewInit.fieldOfView) {
|
|
fov = {
|
|
upDegrees: fakeXRViewInit.fieldOfView.upDegrees,
|
|
downDegrees: fakeXRViewInit.fieldOfView.downDegrees,
|
|
leftDegrees: fakeXRViewInit.fieldOfView.leftDegrees,
|
|
rightDegrees: fakeXRViewInit.fieldOfView.rightDegrees
|
|
};
|
|
} else {
|
|
fov = this._getFovFromProjectionMatrix(fakeXRViewInit.projectionMatrix);
|
|
}
|
|
|
|
let viewEye = vrMojom.XREye.kNone;
|
|
// The eye passed in corresponds to the values in the WebXR spec, which are
|
|
// the strings "none", "left", and "right". They should be converted to the
|
|
// corresponding values of XREye in vr_service.mojom.
|
|
switch(fakeXRViewInit.eye) {
|
|
case "none":
|
|
viewEye = vrMojom.XREye.kNone;
|
|
break;
|
|
case "left":
|
|
viewEye = vrMojom.XREye.kLeft;
|
|
break;
|
|
case "right":
|
|
viewEye = vrMojom.XREye.kRight;
|
|
break;
|
|
}
|
|
|
|
return {
|
|
eye: viewEye,
|
|
geometry: {
|
|
fieldOfView: fov,
|
|
mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform(fakeXRViewInit.viewOffset)),
|
|
// Mojo will ignore extra members, we stash the raw projection matrix
|
|
// here for ease of use with the depth extensions.
|
|
projectionMatrix: fakeXRViewInit.projectionMatrix,
|
|
},
|
|
viewport: {
|
|
x: xOffset,
|
|
y: 0,
|
|
width: fakeXRViewInit.resolution.width,
|
|
height: fakeXRViewInit.resolution.height
|
|
},
|
|
isFirstPersonObserver: fakeXRViewInit.isFirstPersonObserver ? true : false,
|
|
viewOffset: composeGFXTransform(fakeXRViewInit.viewOffset)
|
|
};
|
|
}
|
|
|
|
_setFeatures(supportedFeatures) {
|
|
function convertFeatureToMojom(feature) {
|
|
if (feature in MockRuntime._featureToMojoMap) {
|
|
return MockRuntime._featureToMojoMap[feature];
|
|
} else {
|
|
return xrSessionMojom.XRSessionFeature.INVALID;
|
|
}
|
|
}
|
|
|
|
this.supportedFeatures_ = [];
|
|
|
|
for (let i = 0; i < supportedFeatures.length; i++) {
|
|
const feature = convertFeatureToMojom(supportedFeatures[i]);
|
|
if (feature !== xrSessionMojom.XRSessionFeature.INVALID) {
|
|
this.supportedFeatures_.push(feature);
|
|
}
|
|
}
|
|
}
|
|
|
|
// These methods are intended to be used by MockXRInputSource only.
|
|
_addInputSource(source) {
|
|
if (!this.input_sources_.has(source.source_id_)) {
|
|
this.input_sources_.set(source.source_id_, source);
|
|
}
|
|
}
|
|
|
|
_removeInputSource(source) {
|
|
this.input_sources_.delete(source.source_id_);
|
|
}
|
|
|
|
// These methods are intended to be used by FakeXRAnchorController only.
|
|
_deleteAnchorController(controllerId) {
|
|
this.anchor_controllers_.delete(controllerId);
|
|
}
|
|
|
|
// Extension point for non-standard modules.
|
|
_injectAdditionalFrameData(options, frameData) {
|
|
}
|
|
|
|
// Mojo function implementations.
|
|
|
|
// XRFrameDataProvider implementation.
|
|
getFrameData(options) {
|
|
return new Promise((resolve) => {
|
|
|
|
const populatePose = () => {
|
|
const mojo_space_reset = this.send_mojo_space_reset_;
|
|
this.send_mojo_space_reset_ = false;
|
|
|
|
if (this.pose_) {
|
|
this.pose_.poseIndex++;
|
|
}
|
|
|
|
// Setting the input_state to null tests a slightly different path than
|
|
// the browser tests where if the last input source is removed, the device
|
|
// code always sends up an empty array, but it's also valid mojom to send
|
|
// up a null array.
|
|
let input_state = null;
|
|
if (this.input_sources_.size > 0) {
|
|
input_state = [];
|
|
for (const input_source of this.input_sources_.values()) {
|
|
input_state.push(input_source._getInputSourceState());
|
|
}
|
|
}
|
|
|
|
let frame_views = this.primaryViews_;
|
|
for (let i = 0; i < this.primaryViews_.length; i++) {
|
|
this.primaryViews_[i].geometry.mojoFromView =
|
|
this._getMojoFromViewerWithOffset(this.primaryViews_[i].viewOffset);
|
|
}
|
|
if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) {
|
|
for (let i = 0; i < this.secondaryViews_.length; i++) {
|
|
this.secondaryViews_[i].geometry.mojoFromView =
|
|
this._getMojoFromViewerWithOffset(this.secondaryViews_[i].viewOffset);
|
|
}
|
|
|
|
frame_views = frame_views.concat(this.secondaryViews_);
|
|
}
|
|
|
|
const frameData = {
|
|
renderInfo: {
|
|
frameId: this.next_frame_id_,
|
|
mojoFromViewer: this.pose_,
|
|
views: frame_views
|
|
},
|
|
mojoSpaceReset: mojo_space_reset,
|
|
inputState: input_state,
|
|
timeDelta: {
|
|
// window.performance.now() is in milliseconds, so convert to microseconds.
|
|
microseconds: BigInt(Math.floor(window.performance.now() * 1000)),
|
|
},
|
|
bufferHolder: null,
|
|
cameraImageSize: this.cameraImage_ ? {
|
|
width: this.cameraImage_.width,
|
|
height: this.cameraImage_.height
|
|
} : null,
|
|
renderingTimeRatio: 0,
|
|
stageParameters: this.stageParameters_,
|
|
stageParametersId: this.stageParametersId_,
|
|
lightEstimationData: this.light_estimate_
|
|
};
|
|
|
|
this.next_frame_id_++;
|
|
|
|
this._calculateHitTestResults(frameData);
|
|
|
|
this._calculateAnchorInformation(frameData);
|
|
|
|
if (options.depthActive) {
|
|
this._calculateDepthInformation(frameData);
|
|
}
|
|
|
|
this._injectAdditionalFrameData(options, frameData);
|
|
|
|
resolve({frameData});
|
|
};
|
|
|
|
if(this.sessionOptions_.mode == xrSessionMojom.XRSessionMode.kInline) {
|
|
// Inline sessions should not have a delay introduced since it causes them
|
|
// to miss a vsync blink-side and delays propagation of changes that happened
|
|
// within a rAFcb by one frame (e.g. setViewerOrigin() calls would take 2 frames
|
|
// to propagate).
|
|
populatePose();
|
|
} else {
|
|
// For immerive sessions, add additional delay to allow for anchor creation
|
|
// promises to run.
|
|
setTimeout(populatePose, 3); // note: according to MDN, the timeout is not exact
|
|
}
|
|
});
|
|
}
|
|
|
|
getEnvironmentIntegrationProvider(environmentProviderRequest) {
|
|
if (this.environmentProviderReceiver_) {
|
|
this.environmentProviderReceiver_.$.close();
|
|
}
|
|
this.environmentProviderReceiver_ =
|
|
new vrMojom.XREnvironmentIntegrationProviderReceiver(this);
|
|
this.environmentProviderReceiver_.$.bindHandle(
|
|
environmentProviderRequest.handle);
|
|
}
|
|
|
|
// XREnvironmentIntegrationProvider implementation:
|
|
subscribeToHitTest(nativeOriginInformation, entityTypes, ray) {
|
|
if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
|
|
// Reject outside of AR.
|
|
return Promise.resolve({
|
|
result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
|
|
subscriptionId : 0n
|
|
});
|
|
}
|
|
|
|
if (!this._nativeOriginKnown(nativeOriginInformation)) {
|
|
return Promise.resolve({
|
|
result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
|
|
subscriptionId : 0n
|
|
});
|
|
}
|
|
|
|
// Reserve the id for hit test source:
|
|
const id = this.next_hit_test_id_++;
|
|
const hitTestParameters = { isTransient: false, profileName: null };
|
|
const controller = new FakeXRHitTestSourceController(id);
|
|
|
|
|
|
return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
|
|
.then((succeeded) => {
|
|
if(succeeded) {
|
|
// Store the subscription information as-is (including controller):
|
|
this.hitTestSubscriptions_.set(id, { nativeOriginInformation, entityTypes, ray, controller });
|
|
|
|
return Promise.resolve({
|
|
result : vrMojom.SubscribeToHitTestResult.SUCCESS,
|
|
subscriptionId : id
|
|
});
|
|
} else {
|
|
return Promise.resolve({
|
|
result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
|
|
subscriptionId : 0n
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
subscribeToHitTestForTransientInput(profileName, entityTypes, ray){
|
|
if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
|
|
// Reject outside of AR.
|
|
return Promise.resolve({
|
|
result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
|
|
subscriptionId : 0n
|
|
});
|
|
}
|
|
|
|
const id = this.next_hit_test_id_++;
|
|
const hitTestParameters = { isTransient: true, profileName: profileName };
|
|
const controller = new FakeXRHitTestSourceController(id);
|
|
|
|
// Check if we have hit test source creation callback.
|
|
// If yes, ask it if the hit test source creation should succeed.
|
|
// If no, for back-compat, assume the hit test source creation succeeded.
|
|
return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
|
|
.then((succeeded) => {
|
|
if(succeeded) {
|
|
// Store the subscription information as-is (including controller):
|
|
this.transientHitTestSubscriptions_.set(id, { profileName, entityTypes, ray, controller });
|
|
|
|
return Promise.resolve({
|
|
result : vrMojom.SubscribeToHitTestResult.SUCCESS,
|
|
subscriptionId : id
|
|
});
|
|
} else {
|
|
return Promise.resolve({
|
|
result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
|
|
subscriptionId : 0n
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
unsubscribeFromHitTest(subscriptionId) {
|
|
let controller = null;
|
|
if(this.transientHitTestSubscriptions_.has(subscriptionId)){
|
|
controller = this.transientHitTestSubscriptions_.get(subscriptionId).controller;
|
|
this.transientHitTestSubscriptions_.delete(subscriptionId);
|
|
} else if(this.hitTestSubscriptions_.has(subscriptionId)){
|
|
controller = this.hitTestSubscriptions_.get(subscriptionId).controller;
|
|
this.hitTestSubscriptions_.delete(subscriptionId);
|
|
}
|
|
|
|
if(controller) {
|
|
controller.deleted = true;
|
|
}
|
|
}
|
|
|
|
createAnchor(nativeOriginInformation, nativeOriginFromAnchor) {
|
|
return new Promise((resolve) => {
|
|
if(this.anchor_creation_callback_ == null) {
|
|
resolve({
|
|
result : vrMojom.CreateAnchorResult.FAILURE,
|
|
anchorId : 0n
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const mojoFromNativeOrigin = this._getMojoFromNativeOrigin(nativeOriginInformation);
|
|
if(mojoFromNativeOrigin == null) {
|
|
resolve({
|
|
result : vrMojom.CreateAnchorResult.FAILURE,
|
|
anchorId : 0n
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const mojoFromAnchor = XRMathHelper.mul4x4(mojoFromNativeOrigin, nativeOriginFromAnchor);
|
|
|
|
const anchorCreationParameters = {
|
|
requestedAnchorOrigin: mojoFromAnchor,
|
|
isAttachedToEntity: false,
|
|
};
|
|
|
|
const anchorController = new FakeXRAnchorController();
|
|
|
|
this.anchor_creation_callback_(anchorCreationParameters, anchorController)
|
|
.then((result) => {
|
|
if(result) {
|
|
// If the test allowed the anchor creation,
|
|
// store the anchor controller & return success.
|
|
|
|
const anchor_id = this.next_anchor_id_;
|
|
this.next_anchor_id_++;
|
|
|
|
this.anchor_controllers_.set(anchor_id, anchorController);
|
|
anchorController.device = this;
|
|
anchorController.id = anchor_id;
|
|
|
|
resolve({
|
|
result : vrMojom.CreateAnchorResult.SUCCESS,
|
|
anchorId : anchor_id
|
|
});
|
|
} else {
|
|
// The test has rejected anchor creation.
|
|
resolve({
|
|
result : vrMojom.CreateAnchorResult.FAILURE,
|
|
anchorId : 0n
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// The test threw an error, treat anchor creation as failed.
|
|
resolve({
|
|
result : vrMojom.CreateAnchorResult.FAILURE,
|
|
anchorId : 0n
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
createPlaneAnchor(planeFromAnchor, planeId) {
|
|
return new Promise((resolve) => {
|
|
|
|
// Not supported yet.
|
|
|
|
resolve({
|
|
result : vrMojom.CreateAnchorResult.FAILURE,
|
|
anchorId : 0n,
|
|
});
|
|
});
|
|
}
|
|
|
|
detachAnchor(anchorId) {}
|
|
|
|
// Utility function
|
|
_requestRuntimeSession(sessionOptions) {
|
|
return this._runtimeSupportsSession(sessionOptions).then((result) => {
|
|
// The JavaScript bindings convert c_style_names to camelCase names.
|
|
const options = {
|
|
transportMethod:
|
|
vrMojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER,
|
|
waitForTransferNotification: true,
|
|
waitForRenderNotification: true,
|
|
waitForGpuFence: false,
|
|
};
|
|
|
|
let submit_frame_sink;
|
|
if (result.supportsSession) {
|
|
submit_frame_sink = {
|
|
clientReceiver: this.presentation_provider_._getClientReceiver(),
|
|
provider: this.presentation_provider_._bindProvider(sessionOptions),
|
|
transportOptions: options
|
|
};
|
|
|
|
const dataProviderPtr = new vrMojom.XRFrameDataProviderRemote();
|
|
this.dataProviderReceiver_ =
|
|
new vrMojom.XRFrameDataProviderReceiver(this);
|
|
this.dataProviderReceiver_.$.bindHandle(
|
|
dataProviderPtr.$.bindNewPipeAndPassReceiver().handle);
|
|
this.sessionOptions_ = sessionOptions;
|
|
|
|
this.sessionClient_ = new vrMojom.XRSessionClientRemote();
|
|
const clientReceiver = this.sessionClient_.$.bindNewPipeAndPassReceiver();
|
|
|
|
const enabled_features = [];
|
|
for (let i = 0; i < sessionOptions.requiredFeatures.length; i++) {
|
|
const feature = sessionOptions.requiredFeatures[i];
|
|
if (this._maybeEnableFeature(feature, sessionOptions)) {
|
|
enabled_features.push(feature);
|
|
} else {
|
|
return Promise.resolve({session: null});
|
|
}
|
|
}
|
|
|
|
for (let i =0; i < sessionOptions.optionalFeatures.length; i++) {
|
|
const feature = sessionOptions.optionalFeatures[i];
|
|
if (this._maybeEnableFeature(feature, sessionOptions)) {
|
|
enabled_features.push(feature);
|
|
}
|
|
}
|
|
|
|
this.enabledFeatures_ = enabled_features;
|
|
|
|
return Promise.resolve({
|
|
session: {
|
|
submitFrameSink: submit_frame_sink,
|
|
dataProvider: dataProviderPtr,
|
|
clientReceiver: clientReceiver,
|
|
enabledFeatures: enabled_features,
|
|
deviceConfig: {
|
|
defaultFramebufferScale: this.defaultFramebufferScale_,
|
|
supportsViewportScaling: true,
|
|
// If depth was not enabled above, this should be null.
|
|
depthConfiguration: this.depthConfiguration_,
|
|
views: this._getDefaultViews(),
|
|
},
|
|
enviromentBlendMode: this.enviromentBlendMode_,
|
|
interactionMode: this.interactionMode_
|
|
}
|
|
});
|
|
} else {
|
|
return Promise.resolve({session: null});
|
|
}
|
|
});
|
|
}
|
|
|
|
_runtimeSupportsSession(options) {
|
|
let result = this.supportedModes_.includes(options.mode);
|
|
return Promise.resolve({
|
|
supportsSession: result,
|
|
});
|
|
}
|
|
|
|
_tryGetDepthConfig(options) {
|
|
if (!options.depthOptions) {
|
|
return null;
|
|
}
|
|
|
|
// At present, there are only two depth usages, and we only support CPU.
|
|
if (options.depthOptions.usagePreferences.length !== 0 &&
|
|
!options.depthOptions.usagePreferences.includes(
|
|
xrSessionMojom.XRDepthUsage.kCPUOptimized)) {
|
|
return null;
|
|
}
|
|
const selectedUsage = xrSessionMojom.XRDepthUsage.kCPUOptimized;
|
|
|
|
let selectedFormat = null;
|
|
if (options.depthOptions.dataFormatPreferences.length === 0) {
|
|
selectedFormat = this.depthSupport_.depthFormats.length === 0 ?
|
|
xrSessionMojom.XRDepthDataFormat.kLuminanceAlpha : this.depthSupport_.depthFormats[0];
|
|
} else {
|
|
for (const dataFormatRequest of options.depthOptions.dataFormatPreferences) {
|
|
if (this.depthSupport_.depthFormats.length === 0 ||
|
|
this.depthSupport_.depthFormats.includes(dataFormatRequest)) {
|
|
selectedFormat = dataFormatRequest;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selectedFormat === null) {
|
|
return null;
|
|
}
|
|
|
|
// Default to our first supported depth type. If it's empty (meaning all),
|
|
// then just default to raw.
|
|
let selectedDepthType = this.depthSupport_.depthTypes.length === 0 ?
|
|
xrSessionMojom.XRDepthType.kRawDepth : this.depthSupport_.depthTypes[0];
|
|
// Try to set the depthType to the earliest requested one if it's supported.
|
|
for (const depthTypeRequest of options.depthOptions.depthTypeRequest) {
|
|
if (this.depthSupport_.depthTypes.length === 0 ||
|
|
this.depthSupport_.depthTypes.includes(depthTypeRequest)) {
|
|
selectedDepthType = depthTypeRequest;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
depthUsage: selectedUsage,
|
|
depthDataFormat: selectedFormat,
|
|
depthType: selectedDepthType,
|
|
};
|
|
}
|
|
|
|
_maybeEnableFeature(feature, options) {
|
|
if (this.supportedFeatures_.indexOf(feature) === -1) {
|
|
return false;
|
|
}
|
|
|
|
switch (feature) {
|
|
case xrSessionMojom.XRSessionFeature.DEPTH:
|
|
this.depthConfiguration_ = this._tryGetDepthConfig(options);
|
|
this.matchDepthView_ = options.depthOptions && options.depthOptions.matchDepthView;
|
|
return this.depthConfiguration_ != null;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Private functions - utilities:
|
|
_nativeOriginKnown(nativeOriginInformation){
|
|
|
|
if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
|
|
if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
|
|
// Unknown input source.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} else if (nativeOriginInformation.referenceSpaceType !== undefined) {
|
|
// Bounded_floor & unbounded ref spaces are not yet supported for AR:
|
|
if (nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kUnbounded
|
|
|| nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kBoundedFloor) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
// Planes and anchors are not yet supported by the mock interface.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Private functions - anchors implementation:
|
|
|
|
// Modifies passed in frameData to add anchor information.
|
|
_calculateAnchorInformation(frameData) {
|
|
if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
|
|
return;
|
|
}
|
|
|
|
frameData.anchorsData = {allAnchorsIds: [], updatedAnchorsData: []};
|
|
for(const [id, controller] of this.anchor_controllers_) {
|
|
frameData.anchorsData.allAnchorsIds.push(id);
|
|
|
|
// Send the entire anchor data over if there was a change since last GetFrameData().
|
|
if(controller.dirty) {
|
|
const anchorData = {id};
|
|
if(!controller.paused) {
|
|
anchorData.mojoFromAnchor = getPoseFromTransform(
|
|
XRMathHelper.decomposeRigidTransform(
|
|
controller._getAnchorOrigin()));
|
|
}
|
|
|
|
controller._markProcessed();
|
|
|
|
frameData.anchorsData.updatedAnchorsData.push(anchorData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Private functions - depth sensing implementation:
|
|
|
|
/**
|
|
* Helper to get a TypedArray view for a given depth format.
|
|
* @param {ArrayBuffer} buffer The ArrayBuffer.
|
|
* @param {xrSessionMojom.XRDepthDataFormat} format The depth format.
|
|
* @return {Uint16Array|Float32Array} A typed array view.
|
|
*/
|
|
static _getTypedArrayForFormat(buffer, format) {
|
|
if (format === xrSessionMojom.XRDepthDataFormat.kFloat32) {
|
|
return new Float32Array(buffer);
|
|
} else { // "luminance-alpha" or "unsigned-short"
|
|
return new Uint16Array(buffer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to get a TypedArray for the given depth format.
|
|
* @param {xrSessionMojom.XRDepthDataFormat} format - The Depth format
|
|
* @param {number} size - The size of the array to be created.
|
|
* @return {Uint16Array|Float32Array} A typed array view.
|
|
*/
|
|
static _getEmptyTypedArrayForFormat(format, size) {
|
|
if (format === xrSessionMojom.XRDepthDataFormat.kFloat32) {
|
|
return new Float32Array(size).fill(0.0);
|
|
} else { // "luminance-alpha" or "unsigned-short" (Uint16)
|
|
return new Uint16Array(size).fill(0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reprojects depth data from a source view to a target view.
|
|
* The returned array will be the same width/height, but will be returned as a
|
|
* Uint8Array of the targetFormat (essentially a byte array that can be sent
|
|
* across mojo), so the overall returned size may be different.
|
|
*
|
|
* @param {ArrayBuffer} sourceDepthArrayBuffer - Raw depth data for the source.
|
|
* @param {number} width - Width of the depth data.
|
|
* @param {number} height - Height of the depth data.
|
|
* @param {xrSessionMojom.XRDepthDataFormat} sourceFormatEnum - Format of the source depth data.
|
|
* @param {Float32Array} sourceClipFromSourceView - Projection matrix for the source view.
|
|
* @param {Float32Array} mojoFromSourceView- Matrix of the transform for the source view.
|
|
* @param {xrSessionMojom.XRDepthDataFormat} targetFormatEnum - Format of the target depth data.
|
|
* @param {Float32Array} targetClipFromTargetView - Projection matrix for the target view.
|
|
* @param {Float32Array} mojoFromTargetView - Matrix of the transform for the target view.
|
|
* @return {Uint8Array | null} The reprojected depth data as an Uint8Array, or null on matrix error.
|
|
*/
|
|
static copyDepthData(
|
|
sourceDepthArrayBuffer, width, height, sourceFormatEnum,
|
|
sourceClipFromSourceView, mojoFromSourceView,
|
|
targetFormatEnum,
|
|
targetClipFromTargetView, mojoFromTargetView) {
|
|
|
|
const targetViewFromTargetClip = XRMathHelper.inverse(targetClipFromTargetView);
|
|
const sourceViewFromMojo = XRMathHelper.inverse(mojoFromSourceView);
|
|
|
|
// Check if any matrices were not supplied or matrix inversions failed.
|
|
if (!targetViewFromTargetClip || !sourceViewFromMojo || !mojoFromTargetView) {
|
|
return null;
|
|
}
|
|
|
|
// Build the full transformation from Target Clip space to Source Clip space.
|
|
const mojoFromTargetClip = XRMathHelper.mul4x4(mojoFromTargetView, targetViewFromTargetClip);
|
|
if (!mojoFromTargetClip) return null;
|
|
|
|
const sourceViewFromTargetClip = XRMathHelper.mul4x4(sourceViewFromMojo, mojoFromTargetClip);
|
|
if (!sourceViewFromTargetClip) return null;
|
|
|
|
const sourceClipFromTargetClip = XRMathHelper.mul4x4(sourceClipFromSourceView, sourceViewFromTargetClip);
|
|
if (!sourceClipFromTargetClip) return null;
|
|
|
|
const sourceTypedArray = MockRuntime._getTypedArrayForFormat(sourceDepthArrayBuffer, sourceFormatEnum);
|
|
let internalTargetDepthTypedArray = MockRuntime._getEmptyTypedArrayForFormat(targetFormatEnum, width * height);
|
|
|
|
// Iterate over target pixels (Backward Mapping)
|
|
for (let ty = 0; ty < height; ++ty) {
|
|
for (let tx = 0; tx < width; ++tx) {
|
|
// Convert target pixel (tx, ty) to target NDC coordinates
|
|
const u_tgt_pixel = (tx + 0.5) / width; // u in [0, 1], Y-down from top-left
|
|
const v_tgt_pixel = (ty + 0.5) / height; // v in [0, 1], Y-down from top-left
|
|
|
|
const ndc_x_tgt = u_tgt_pixel * 2.0 - 1.0; // NDC X in [-1, 1]
|
|
const ndc_y_tgt = 1.0 - v_tgt_pixel * 2.0; // NDC Y in [-1, 1], Y-up
|
|
|
|
// Define a point on the near plane in target clip space
|
|
const P_clip_tgt = { x: ndc_x_tgt, y: ndc_y_tgt, z: -1.0, w: 1.0 };
|
|
|
|
// Transform this point to source clip space
|
|
const P_clip_src = XRMathHelper.transform_by_matrix(sourceClipFromTargetClip, P_clip_tgt);
|
|
|
|
// Homogenize to get source NDC coordinates
|
|
if (Math.abs(P_clip_src.w) < XRMathHelper.EPSILON) {
|
|
internalTargetDepthTypedArray[ty * width + tx] = 0; // Cannot project
|
|
continue;
|
|
}
|
|
const ndc_x_src = P_clip_src.x / P_clip_src.w;
|
|
const ndc_y_src = P_clip_src.y / P_clip_src.w;
|
|
|
|
// Convert source NDC to source pixel coordinates
|
|
const u_src_pixel = (ndc_x_src + 1.0) / 2.0;
|
|
const v_src_pixel = (1.0 - ndc_y_src) / 2.0; // Convert source NDC Y-up to pixel Y-down
|
|
|
|
const sx = Math.floor(u_src_pixel * width);
|
|
const sy = Math.floor(v_src_pixel * height);
|
|
|
|
let target_raw_depth = 0; // Default to 0 (no data)
|
|
|
|
// Check if the calculated source pixel is within bounds
|
|
if (sx >= 0 && sx < width && sy >= 0 && sy < height) {
|
|
const source_raw_value = sourceTypedArray[sy * width + sx];
|
|
|
|
let isValidSourceDepth = false;
|
|
if (sourceFormatEnum === xrSessionMojom.XRDepthDataFormat.kFloat32) {
|
|
if (source_raw_value > 0 && isFinite(source_raw_value)) {
|
|
isValidSourceDepth = true;
|
|
}
|
|
} else { // Uint16 source
|
|
if (source_raw_value > 0) {
|
|
isValidSourceDepth = true;
|
|
}
|
|
}
|
|
|
|
if (isValidSourceDepth) {
|
|
if (targetFormatEnum === xrSessionMojom.XRDepthDataFormat.kFloat32) {
|
|
target_raw_depth = source_raw_value;
|
|
} else {
|
|
// Clamp to the valid range for Uint16
|
|
target_raw_depth = Math.max(0, Math.min(0xFFFF, Math.round(source_raw_value)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not in bounds or source depth invalid, target_raw_depth remains 0.
|
|
internalTargetDepthTypedArray[ty * width + tx] = target_raw_depth;
|
|
}
|
|
}
|
|
|
|
return new Uint8Array(internalTargetDepthTypedArray.buffer);
|
|
}
|
|
|
|
_getDepthPixelData(depthGeometry) {
|
|
if (!this.matchDepthView_ || !depthGeometry) {
|
|
return { bytes: this.depthSensingData_.depthData };
|
|
}
|
|
|
|
const sourceProjectionMatrix = depthGeometry.projectionMatrix;
|
|
const sourceViewOffset = depthGeometry.mojoFromView;
|
|
if (!sourceProjectionMatrix || !sourceViewOffset) {
|
|
return { bytes: this.depthSensingData_.depthData };
|
|
}
|
|
|
|
if (this.primaryViews_.length === 0) {
|
|
return { bytes: this.depthSensingData_.depthData };
|
|
}
|
|
|
|
const targetView = this.primaryViews_[0];
|
|
const targetProjectionMatrix = targetView.geometry.projectionMatrix;
|
|
const targetViewOffset = targetView.geometry.mojoFromView;
|
|
if (!targetProjectionMatrix || !targetViewOffset) {
|
|
return { bytes: this.depthSensingData_.depthData };
|
|
}
|
|
|
|
return { bytes: MockRuntime.copyDepthData(
|
|
this.depthSensingData_.depthData,
|
|
this.depthSensingData_.width,
|
|
this.depthSensingData_.height,
|
|
MockRuntime._depthDataFormatToMojoMap[this.depthSensingData_.depthFormat],
|
|
sourceProjectionMatrix,
|
|
sourceViewOffset.matrix,
|
|
this.depthConfiguration_.depthDataFormat,
|
|
targetProjectionMatrix,
|
|
targetViewOffset.matrix
|
|
)};
|
|
}
|
|
|
|
// Modifies passed in frameData to add anchor information.
|
|
_calculateDepthInformation(frameData) {
|
|
if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
|
|
return;
|
|
}
|
|
|
|
if (!this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.DEPTH)) {
|
|
return;
|
|
}
|
|
|
|
let newDepthData;
|
|
|
|
// If we don't have a current depth data, we'll return null
|
|
// (i.e. no data is not a valid data, so it cannot be "StillValid").
|
|
if (this.depthSensingData_ == null) {
|
|
newDepthData = null;
|
|
} else if(!this.depthSensingDataDirty_) {
|
|
newDepthData = { dataStillValid: {}};
|
|
} else {
|
|
let viewGeometry = null;
|
|
const projectionMatrix = this.depthSensingData_.projectionMatrix;
|
|
const viewOffset = this.depthSensingData_.viewOffset;
|
|
|
|
if (projectionMatrix && viewOffset) {
|
|
const fov = this._getFovFromProjectionMatrix(projectionMatrix);
|
|
|
|
viewGeometry = {
|
|
fieldOfView: fov,
|
|
mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform(viewOffset)),
|
|
// Convenience member for `_getDepthPixelData`
|
|
projectionMatrix: projectionMatrix
|
|
};
|
|
}
|
|
|
|
newDepthData = {
|
|
updatedDepthData: {
|
|
timeDelta: frameData.timeDelta,
|
|
normTextureFromNormView: this.depthSensingData_.normDepthBufferFromNormView,
|
|
rawValueToMeters: this.depthSensingData_.rawValueToMeters,
|
|
size: { width: this.depthSensingData_.width, height: this.depthSensingData_.height },
|
|
pixelData: this._getDepthPixelData(viewGeometry),
|
|
viewGeometry: this.matchDepthView_ ? null : viewGeometry
|
|
}
|
|
};
|
|
}
|
|
|
|
for (let i = 0; i < this.primaryViews_.length; i++) {
|
|
this.primaryViews_[i].depthData = newDepthData;
|
|
}
|
|
if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) {
|
|
for (let i = 0; i < this.secondaryViews_.length; i++) {
|
|
this.secondaryViews_[i].depthData = newDepthData;
|
|
}
|
|
}
|
|
|
|
this.depthSensingDataDirty_ = false;
|
|
}
|
|
|
|
// Private functions - hit test implementation:
|
|
|
|
// Returns a Promise<bool> that signifies whether hit test source creation should succeed.
|
|
// If we have a hit test source creation callback installed, invoke it and return its result.
|
|
// If it's not installed, for back-compat just return a promise that resolves to true.
|
|
_shouldHitTestSourceCreationSucceed(hitTestParameters, controller) {
|
|
if(this.hit_test_source_creation_callback_) {
|
|
return this.hit_test_source_creation_callback_(hitTestParameters, controller);
|
|
} else {
|
|
return Promise.resolve(true);
|
|
}
|
|
}
|
|
|
|
// Modifies passed in frameData to add hit test results.
|
|
_calculateHitTestResults(frameData) {
|
|
if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
|
|
return;
|
|
}
|
|
|
|
frameData.hitTestSubscriptionResults = {results: [],
|
|
transientInputResults: []};
|
|
if (!this.world_) {
|
|
return;
|
|
}
|
|
|
|
// Non-transient hit test:
|
|
for (const [id, subscription] of this.hitTestSubscriptions_) {
|
|
const mojo_from_native_origin = this._getMojoFromNativeOrigin(subscription.nativeOriginInformation);
|
|
if (!mojo_from_native_origin) continue;
|
|
|
|
const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
|
|
subscription.ray,
|
|
mojo_from_native_origin
|
|
);
|
|
|
|
const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
|
|
frameData.hitTestSubscriptionResults.results.push(
|
|
{subscriptionId: id, hitTestResults: results});
|
|
}
|
|
|
|
// Transient hit test:
|
|
const mojo_from_viewer = this._getMojoFromViewer();
|
|
|
|
for (const [id, subscription] of this.transientHitTestSubscriptions_) {
|
|
const result = {subscriptionId: id,
|
|
inputSourceIdToHitTestResults: new Map()};
|
|
|
|
// Find all input sources that match the profile name:
|
|
const matching_input_sources = Array.from(this.input_sources_.values())
|
|
.filter(input_source => input_source.profiles_.includes(subscription.profileName));
|
|
|
|
for (const input_source of matching_input_sources) {
|
|
const mojo_from_native_origin = input_source._getMojoFromInputSource(mojo_from_viewer);
|
|
|
|
const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
|
|
subscription.ray,
|
|
mojo_from_native_origin
|
|
);
|
|
|
|
const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
|
|
|
|
result.inputSourceIdToHitTestResults.set(input_source.source_id_, results);
|
|
}
|
|
|
|
frameData.hitTestSubscriptionResults.transientInputResults.push(result);
|
|
}
|
|
}
|
|
|
|
// Returns 2-element array [origin, direction] of a ray in mojo space.
|
|
// |ray| is expressed relative to native origin.
|
|
_transformRayToMojoSpace(ray, mojo_from_native_origin) {
|
|
const ray_origin = {
|
|
x: ray.origin.x,
|
|
y: ray.origin.y,
|
|
z: ray.origin.z,
|
|
w: 1
|
|
};
|
|
const ray_direction = {
|
|
x: ray.direction.x,
|
|
y: ray.direction.y,
|
|
z: ray.direction.z,
|
|
w: 0
|
|
};
|
|
|
|
const mojo_ray_origin = XRMathHelper.transform_by_matrix(
|
|
mojo_from_native_origin,
|
|
ray_origin);
|
|
const mojo_ray_direction = XRMathHelper.transform_by_matrix(
|
|
mojo_from_native_origin,
|
|
ray_direction);
|
|
|
|
return [mojo_ray_origin, mojo_ray_direction];
|
|
}
|
|
|
|
// Hit tests the passed in ray (expressed as origin and direction) against the mocked world data.
|
|
_hitTestWorld(origin, direction, entityTypes) {
|
|
let result = [];
|
|
|
|
for (const region of this.world_.hitTestRegions) {
|
|
const partial_result = this._hitTestRegion(
|
|
region,
|
|
origin, direction,
|
|
entityTypes);
|
|
|
|
result = result.concat(partial_result);
|
|
}
|
|
|
|
return result.sort((lhs, rhs) => lhs.distance - rhs.distance).map((hitTest) => {
|
|
delete hitTest.distance;
|
|
return hitTest;
|
|
});
|
|
}
|
|
|
|
// Hit tests the passed in ray (expressed as origin and direction) against world region.
|
|
// |entityTypes| is a set of FakeXRRegionTypes.
|
|
// |region| is FakeXRRegion.
|
|
// Returns array of XRHitResults, each entry will be decorated with the distance from the ray origin (along the ray).
|
|
_hitTestRegion(region, origin, direction, entityTypes) {
|
|
const regionNameToMojoEnum = {
|
|
"point": vrMojom.EntityTypeForHitTest.POINT,
|
|
"plane": vrMojom.EntityTypeForHitTest.PLANE,
|
|
"mesh":null
|
|
};
|
|
|
|
if (!entityTypes.includes(regionNameToMojoEnum[region.type])) {
|
|
return [];
|
|
}
|
|
|
|
const result = [];
|
|
for (const face of region.faces) {
|
|
const maybe_hit = this._hitTestFace(face, origin, direction);
|
|
if (maybe_hit) {
|
|
result.push(maybe_hit);
|
|
}
|
|
}
|
|
|
|
// The results should be sorted by distance and there should be no 2 entries with
|
|
// the same distance from ray origin - that would mean they are the same point.
|
|
// This situation is possible when a ray intersects the region through an edge shared
|
|
// by 2 faces.
|
|
return result.sort((lhs, rhs) => lhs.distance - rhs.distance)
|
|
.filter((val, index, array) => index === 0 || val.distance !== array[index - 1].distance);
|
|
}
|
|
|
|
// Hit tests the passed in ray (expressed as origin and direction) against a single face.
|
|
// |face|, |origin|, and |direction| are specified in world (aka mojo) coordinates.
|
|
// |face| is an array of DOMPointInits.
|
|
// Returns null if the face does not intersect with the ray, otherwise the result is
|
|
// an XRHitResult with matrix describing the pose of the intersection point.
|
|
_hitTestFace(face, origin, direction) {
|
|
const add = XRMathHelper.add;
|
|
const sub = XRMathHelper.sub;
|
|
const mul = XRMathHelper.mul;
|
|
const normalize = XRMathHelper.normalize;
|
|
const dot = XRMathHelper.dot;
|
|
const cross = XRMathHelper.cross;
|
|
const neg = XRMathHelper.neg;
|
|
|
|
//1. Calculate plane normal in world coordinates.
|
|
const point_A = face.vertices[0];
|
|
const point_B = face.vertices[1];
|
|
const point_C = face.vertices[2];
|
|
|
|
const edge_AB = sub(point_B, point_A);
|
|
const edge_AC = sub(point_C, point_A);
|
|
|
|
const normal = normalize(cross(edge_AB, edge_AC));
|
|
|
|
const numerator = dot(sub(point_A, origin), normal);
|
|
const denominator = dot(direction, normal);
|
|
|
|
if (Math.abs(denominator) < XRMathHelper.EPSILON) {
|
|
// Planes are nearly parallel - there's either infinitely many intersection points or 0.
|
|
// Both cases signify a "no hit" for us.
|
|
return null;
|
|
} else {
|
|
// Single intersection point between the infinite plane and the line (*not* ray).
|
|
// Need to calculate the hit test matrix taking into account the face vertices.
|
|
const distance = numerator / denominator;
|
|
if (distance < 0) {
|
|
// Line - plane intersection exists, but not the half-line - plane does not.
|
|
return null;
|
|
} else {
|
|
const intersection_point = add(origin, mul(distance, direction));
|
|
// Since we are treating the face as a solid, flip the normal so that its
|
|
// half-space will contain the ray origin.
|
|
const y_axis = denominator > 0 ? neg(normal) : normal;
|
|
|
|
let z_axis = null;
|
|
const cos_direction_and_y_axis = dot(direction, y_axis);
|
|
if (Math.abs(cos_direction_and_y_axis) > (1 - XRMathHelper.EPSILON)) {
|
|
// Ray and the hit test normal are co-linear - try using the 'up' or 'right' vector's projection on the face plane as the Z axis.
|
|
// Note: this edge case is currently not covered by the spec.
|
|
const up = {x: 0.0, y: 1.0, z: 0.0, w: 0.0};
|
|
const right = {x: 1.0, y: 0.0, z: 0.0, w: 0.0};
|
|
|
|
z_axis = Math.abs(dot(up, y_axis)) > (1 - XRMathHelper.EPSILON)
|
|
? sub(up, mul(dot(right, y_axis), y_axis)) // `up is also co-linear with hit test normal, use `right`
|
|
: sub(up, mul(dot(up, y_axis), y_axis)); // `up` is not co-linear with hit test normal, use it
|
|
} else {
|
|
// Project the ray direction onto the plane, negate it and use as a Z axis.
|
|
z_axis = neg(sub(direction, mul(cos_direction_and_y_axis, y_axis))); // Z should point towards the ray origin, not away.
|
|
}
|
|
|
|
z_axis = normalize(z_axis);
|
|
const x_axis = normalize(cross(y_axis, z_axis));
|
|
|
|
// Filter out the points not in polygon.
|
|
if (!XRMathHelper.pointInFace(intersection_point, face)) {
|
|
return null;
|
|
}
|
|
|
|
const hitResult = {planeId: 0n};
|
|
hitResult.distance = distance; // Extend the object with additional information used by higher layers.
|
|
// It will not be serialized over mojom.
|
|
|
|
const matrix = new Array(16);
|
|
|
|
matrix[0] = x_axis.x;
|
|
matrix[1] = x_axis.y;
|
|
matrix[2] = x_axis.z;
|
|
matrix[3] = 0;
|
|
|
|
matrix[4] = y_axis.x;
|
|
matrix[5] = y_axis.y;
|
|
matrix[6] = y_axis.z;
|
|
matrix[7] = 0;
|
|
|
|
matrix[8] = z_axis.x;
|
|
matrix[9] = z_axis.y;
|
|
matrix[10] = z_axis.z;
|
|
matrix[11] = 0;
|
|
|
|
matrix[12] = intersection_point.x;
|
|
matrix[13] = intersection_point.y;
|
|
matrix[14] = intersection_point.z;
|
|
matrix[15] = 1;
|
|
|
|
hitResult.mojoFromResult = getPoseFromTransform(
|
|
XRMathHelper.decomposeRigidTransform(matrix));
|
|
return hitResult;
|
|
}
|
|
}
|
|
}
|
|
|
|
_getMojoFromViewer() {
|
|
if (!this.pose_) {
|
|
return XRMathHelper.identity();
|
|
}
|
|
const transform = {
|
|
position: [
|
|
this.pose_.position.x,
|
|
this.pose_.position.y,
|
|
this.pose_.position.z],
|
|
orientation: [
|
|
this.pose_.orientation.x,
|
|
this.pose_.orientation.y,
|
|
this.pose_.orientation.z,
|
|
this.pose_.orientation.w],
|
|
};
|
|
|
|
return getMatrixFromTransform(transform);
|
|
}
|
|
|
|
_getMojoFromViewerWithOffset(viewOffset) {
|
|
return { matrix: XRMathHelper.mul4x4(this._getMojoFromViewer(), viewOffset.matrix) };
|
|
}
|
|
|
|
_getMojoFromNativeOrigin(nativeOriginInformation) {
|
|
const mojo_from_viewer = this._getMojoFromViewer();
|
|
|
|
if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
|
|
if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
|
|
return null;
|
|
} else {
|
|
const inputSource = this.input_sources_.get(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId);
|
|
return inputSource._getMojoFromInputSource(mojo_from_viewer);
|
|
}
|
|
} else if (nativeOriginInformation.referenceSpaceType !== undefined) {
|
|
switch (nativeOriginInformation.referenceSpaceType) {
|
|
case vrMojom.XRReferenceSpaceType.kLocal:
|
|
return XRMathHelper.identity();
|
|
case vrMojom.XRReferenceSpaceType.kLocalFloor:
|
|
if (this.stageParameters_ == null || this.stageParameters_.mojoFromStage == null) {
|
|
console.warn("Standing transform not available.");
|
|
return null;
|
|
}
|
|
return this.stageParameters_.mojoFromStage.matrix;
|
|
case vrMojom.XRReferenceSpaceType.kViewer:
|
|
return mojo_from_viewer;
|
|
case vrMojom.XRReferenceSpaceType.kBoundedFloor:
|
|
return null;
|
|
case vrMojom.XRReferenceSpaceType.kUnbounded:
|
|
return null;
|
|
default:
|
|
throw new TypeError("Unrecognized XRReferenceSpaceType!");
|
|
}
|
|
} else {
|
|
// Anchors & planes are not yet supported for hit test.
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class MockXRInputSource {
|
|
constructor(fakeInputSourceInit, id, pairedDevice) {
|
|
this.source_id_ = id;
|
|
this.pairedDevice_ = pairedDevice;
|
|
this.handedness_ = fakeInputSourceInit.handedness;
|
|
this.target_ray_mode_ = fakeInputSourceInit.targetRayMode;
|
|
|
|
if (fakeInputSourceInit.pointerOrigin == null) {
|
|
throw new TypeError("FakeXRInputSourceInit.pointerOrigin is required.");
|
|
}
|
|
|
|
this.setPointerOrigin(fakeInputSourceInit.pointerOrigin);
|
|
this.setProfiles(fakeInputSourceInit.profiles);
|
|
|
|
this.primary_input_pressed_ = false;
|
|
if (fakeInputSourceInit.selectionStarted != null) {
|
|
this.primary_input_pressed_ = fakeInputSourceInit.selectionStarted;
|
|
}
|
|
|
|
this.primary_input_clicked_ = false;
|
|
if (fakeInputSourceInit.selectionClicked != null) {
|
|
this.primary_input_clicked_ = fakeInputSourceInit.selectionClicked;
|
|
}
|
|
|
|
this.primary_squeeze_pressed_ = false;
|
|
this.primary_squeeze_clicked_ = false;
|
|
|
|
this.mojo_from_input_ = null;
|
|
if (fakeInputSourceInit.gripOrigin != null) {
|
|
this.setGripOrigin(fakeInputSourceInit.gripOrigin);
|
|
}
|
|
|
|
// This properly handles if supportedButtons were not specified.
|
|
this.setSupportedButtons(fakeInputSourceInit.supportedButtons);
|
|
|
|
this.emulated_position_ = false;
|
|
this.desc_dirty_ = true;
|
|
}
|
|
|
|
// WebXR Test API
|
|
setHandedness(handedness) {
|
|
if (this.handedness_ != handedness) {
|
|
this.desc_dirty_ = true;
|
|
this.handedness_ = handedness;
|
|
}
|
|
}
|
|
|
|
setTargetRayMode(targetRayMode) {
|
|
if (this.target_ray_mode_ != targetRayMode) {
|
|
this.desc_dirty_ = true;
|
|
this.target_ray_mode_ = targetRayMode;
|
|
}
|
|
}
|
|
|
|
setProfiles(profiles) {
|
|
this.desc_dirty_ = true;
|
|
this.profiles_ = profiles;
|
|
}
|
|
|
|
setGripOrigin(transform, emulatedPosition = false) {
|
|
// grip_origin was renamed to mojo_from_input in mojo
|
|
this.mojo_from_input_ = composeGFXTransform(transform);
|
|
this.emulated_position_ = emulatedPosition;
|
|
|
|
// Technically, setting the grip shouldn't make the description dirty, but
|
|
// the webxr-test-api sets our pointer as mojoFromPointer; however, we only
|
|
// support it across mojom as inputFromPointer, so we need to recalculate it
|
|
// whenever the grip moves.
|
|
this.desc_dirty_ = true;
|
|
}
|
|
|
|
clearGripOrigin() {
|
|
// grip_origin was renamed to mojo_from_input in mojo
|
|
if (this.mojo_from_input_ != null) {
|
|
this.mojo_from_input_ = null;
|
|
this.emulated_position_ = false;
|
|
this.desc_dirty_ = true;
|
|
}
|
|
}
|
|
|
|
setPointerOrigin(transform, emulatedPosition = false) {
|
|
// pointer_origin is mojo_from_pointer.
|
|
this.desc_dirty_ = true;
|
|
this.mojo_from_pointer_ = composeGFXTransform(transform);
|
|
this.emulated_position_ = emulatedPosition;
|
|
}
|
|
|
|
disconnect() {
|
|
this.pairedDevice_._removeInputSource(this);
|
|
}
|
|
|
|
reconnect() {
|
|
this.pairedDevice_._addInputSource(this);
|
|
}
|
|
|
|
startSelection() {
|
|
this.primary_input_pressed_ = true;
|
|
if (this.gamepad_) {
|
|
this.gamepad_.buttons[0].pressed = true;
|
|
this.gamepad_.buttons[0].touched = true;
|
|
}
|
|
}
|
|
|
|
endSelection() {
|
|
if (!this.primary_input_pressed_) {
|
|
throw new Error("Attempted to end selection which was not started");
|
|
}
|
|
|
|
this.primary_input_pressed_ = false;
|
|
this.primary_input_clicked_ = true;
|
|
|
|
if (this.gamepad_) {
|
|
this.gamepad_.buttons[0].pressed = false;
|
|
this.gamepad_.buttons[0].touched = false;
|
|
}
|
|
}
|
|
|
|
simulateSelect() {
|
|
this.primary_input_clicked_ = true;
|
|
}
|
|
|
|
setSupportedButtons(supportedButtons) {
|
|
this.gamepad_ = null;
|
|
this.supported_buttons_ = [];
|
|
|
|
// If there are no supported buttons, we can stop now.
|
|
if (supportedButtons == null || supportedButtons.length < 1) {
|
|
return;
|
|
}
|
|
|
|
const supported_button_map = {};
|
|
this.gamepad_ = this._getEmptyGamepad();
|
|
for (let i = 0; i < supportedButtons.length; i++) {
|
|
const buttonType = supportedButtons[i].buttonType;
|
|
this.supported_buttons_.push(buttonType);
|
|
supported_button_map[buttonType] = supportedButtons[i];
|
|
}
|
|
|
|
// Let's start by building the button state in order of priority:
|
|
// Primary button is index 0.
|
|
this.gamepad_.buttons.push({
|
|
pressed: this.primary_input_pressed_,
|
|
touched: this.primary_input_pressed_,
|
|
value: this.primary_input_pressed_ ? 1.0 : 0.0
|
|
});
|
|
|
|
// Now add the rest of our buttons
|
|
this._addGamepadButton(supported_button_map['grip']);
|
|
this._addGamepadButton(supported_button_map['touchpad']);
|
|
this._addGamepadButton(supported_button_map['thumbstick']);
|
|
this._addGamepadButton(supported_button_map['optional-button']);
|
|
this._addGamepadButton(supported_button_map['optional-thumbstick']);
|
|
|
|
// Finally, back-fill placeholder buttons/axes
|
|
for (let i = 0; i < this.gamepad_.buttons.length; i++) {
|
|
if (this.gamepad_.buttons[i] == null) {
|
|
this.gamepad_.buttons[i] = {
|
|
pressed: false,
|
|
touched: false,
|
|
value: 0
|
|
};
|
|
}
|
|
}
|
|
|
|
for (let i=0; i < this.gamepad_.axes.length; i++) {
|
|
if (this.gamepad_.axes[i] == null) {
|
|
this.gamepad_.axes[i] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateButtonState(buttonState) {
|
|
if (this.supported_buttons_.indexOf(buttonState.buttonType) == -1) {
|
|
throw new Error("Tried to update state on an unsupported button");
|
|
}
|
|
|
|
const buttonIndex = this._getButtonIndex(buttonState.buttonType);
|
|
const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
|
|
|
|
if (buttonIndex == -1) {
|
|
throw new Error("Unknown Button Type!");
|
|
}
|
|
|
|
// is this a 'squeeze' button?
|
|
if (buttonIndex === this._getButtonIndex('grip')) {
|
|
// squeeze
|
|
if (buttonState.pressed) {
|
|
this.primary_squeeze_pressed_ = true;
|
|
} else if (this.gamepad_.buttons[buttonIndex].pressed) {
|
|
this.primary_squeeze_clicked_ = true;
|
|
this.primary_squeeze_pressed_ = false;
|
|
} else {
|
|
this.primary_squeeze_clicked_ = false;
|
|
this.primary_squeeze_pressed_ = false;
|
|
}
|
|
}
|
|
|
|
this.gamepad_.buttons[buttonIndex].pressed = buttonState.pressed;
|
|
this.gamepad_.buttons[buttonIndex].touched = buttonState.touched;
|
|
this.gamepad_.buttons[buttonIndex].value = buttonState.pressedValue;
|
|
|
|
if (axesStartIndex != -1) {
|
|
this.gamepad_.axes[axesStartIndex] = buttonState.xValue == null ? 0.0 : buttonState.xValue;
|
|
this.gamepad_.axes[axesStartIndex + 1] = buttonState.yValue == null ? 0.0 : buttonState.yValue;
|
|
}
|
|
}
|
|
|
|
// DOM Overlay Extensions
|
|
setOverlayPointerPosition(x, y) {
|
|
this.overlay_pointer_position_ = {x: x, y: y};
|
|
}
|
|
|
|
// Helpers for Mojom
|
|
_getInputSourceState() {
|
|
const input_state = {};
|
|
|
|
input_state.sourceId = this.source_id_;
|
|
input_state.isAuxiliary = false;
|
|
|
|
input_state.primaryInputPressed = this.primary_input_pressed_;
|
|
input_state.primaryInputClicked = this.primary_input_clicked_;
|
|
|
|
input_state.primarySqueezePressed = this.primary_squeeze_pressed_;
|
|
input_state.primarySqueezeClicked = this.primary_squeeze_clicked_;
|
|
// Setting the input source's "clicked" state should generate one "select"
|
|
// event. Reset the input value to prevent it from continuously generating
|
|
// events.
|
|
this.primary_input_clicked_ = false;
|
|
// Setting the input source's "clicked" state should generate one "squeeze"
|
|
// event. Reset the input value to prevent it from continuously generating
|
|
// events.
|
|
this.primary_squeeze_clicked_ = false;
|
|
|
|
input_state.mojoFromInput = this.mojo_from_input_;
|
|
|
|
input_state.gamepad = this.gamepad_;
|
|
|
|
input_state.emulatedPosition = this.emulated_position_;
|
|
|
|
if (this.desc_dirty_) {
|
|
const input_desc = {};
|
|
|
|
switch (this.target_ray_mode_) {
|
|
case 'gaze':
|
|
input_desc.targetRayMode = vrMojom.XRTargetRayMode.GAZING;
|
|
break;
|
|
case 'tracked-pointer':
|
|
input_desc.targetRayMode = vrMojom.XRTargetRayMode.POINTING;
|
|
break;
|
|
case 'screen':
|
|
input_desc.targetRayMode = vrMojom.XRTargetRayMode.TAPPING;
|
|
break;
|
|
default:
|
|
throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
|
|
}
|
|
|
|
switch (this.handedness_) {
|
|
case 'left':
|
|
input_desc.handedness = vrMojom.XRHandedness.LEFT;
|
|
break;
|
|
case 'right':
|
|
input_desc.handedness = vrMojom.XRHandedness.RIGHT;
|
|
break;
|
|
default:
|
|
input_desc.handedness = vrMojom.XRHandedness.NONE;
|
|
break;
|
|
}
|
|
|
|
// Mojo requires us to send the pointerOrigin as relative to the grip
|
|
// space. If we don't have a grip space, we'll just assume that there
|
|
// is a grip at identity. This allows tests to simulate controllers that
|
|
// are really just a pointer with no tracked grip, though we will end up
|
|
// exposing that grip space.
|
|
let mojo_from_input = XRMathHelper.identity();
|
|
switch (this.target_ray_mode_) {
|
|
case 'gaze':
|
|
case 'screen':
|
|
// For gaze and screen space, we won't have a mojo_from_input; however
|
|
// the "input" position is just the viewer, so use mojo_from_viewer.
|
|
mojo_from_input = this.pairedDevice_._getMojoFromViewer();
|
|
break;
|
|
case 'tracked-pointer':
|
|
// If we have a tracked grip position (e.g. mojo_from_input), then use
|
|
// that. If we don't, then we'll just set the pointer offset directly,
|
|
// using identity as set above.
|
|
if (this.mojo_from_input_) {
|
|
mojo_from_input = this.mojo_from_input_.matrix;
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
|
|
}
|
|
|
|
// To convert mojo_from_pointer to input_from_pointer, we need:
|
|
// input_from_pointer = input_from_mojo * mojo_from_pointer
|
|
// Since we store mojo_from_input, we need to invert it here before
|
|
// multiplying.
|
|
let input_from_mojo = XRMathHelper.inverse(mojo_from_input);
|
|
input_desc.inputFromPointer = {};
|
|
input_desc.inputFromPointer.matrix =
|
|
XRMathHelper.mul4x4(input_from_mojo, this.mojo_from_pointer_.matrix);
|
|
|
|
input_desc.profiles = this.profiles_;
|
|
|
|
input_state.description = input_desc;
|
|
|
|
this.desc_dirty_ = false;
|
|
}
|
|
|
|
// Pointer data for DOM Overlay, set by setOverlayPointerPosition()
|
|
if (this.overlay_pointer_position_) {
|
|
input_state.overlayPointerPosition = this.overlay_pointer_position_;
|
|
this.overlay_pointer_position_ = null;
|
|
}
|
|
|
|
return input_state;
|
|
}
|
|
|
|
_getEmptyGamepad() {
|
|
// Mojo complains if some of the properties on Gamepad are null, so set
|
|
// everything to reasonable defaults that tests can override.
|
|
const gamepad = {
|
|
connected: true,
|
|
id: [],
|
|
timestamp: 0n,
|
|
axes: [],
|
|
buttons: [],
|
|
touchEvents: [],
|
|
mapping: GamepadMapping.GamepadMappingStandard,
|
|
displayId: 0,
|
|
};
|
|
|
|
switch (this.handedness_) {
|
|
case 'left':
|
|
gamepad.hand = GamepadHand.GamepadHandLeft;
|
|
break;
|
|
case 'right':
|
|
gamepad.hand = GamepadHand.GamepadHandRight;
|
|
break;
|
|
default:
|
|
gamepad.hand = GamepadHand.GamepadHandNone;
|
|
break;
|
|
}
|
|
|
|
return gamepad;
|
|
}
|
|
|
|
_addGamepadButton(buttonState) {
|
|
if (buttonState == null) {
|
|
return;
|
|
}
|
|
|
|
const buttonIndex = this._getButtonIndex(buttonState.buttonType);
|
|
const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
|
|
|
|
if (buttonIndex == -1) {
|
|
throw new Error("Unknown Button Type!");
|
|
}
|
|
|
|
this.gamepad_.buttons[buttonIndex] = {
|
|
pressed: buttonState.pressed,
|
|
touched: buttonState.touched,
|
|
value: buttonState.pressedValue
|
|
};
|
|
|
|
// Add x/y value if supported.
|
|
if (axesStartIndex != -1) {
|
|
this.gamepad_.axes[axesStartIndex] = (buttonState.xValue == null ? 0.0 : buttonSate.xValue);
|
|
this.gamepad_.axes[axesStartIndex + 1] = (buttonState.yValue == null ? 0.0 : buttonSate.yValue);
|
|
}
|
|
}
|
|
|
|
// General Helper methods
|
|
_getButtonIndex(buttonType) {
|
|
switch (buttonType) {
|
|
case 'grip':
|
|
return 1;
|
|
case 'touchpad':
|
|
return 2;
|
|
case 'thumbstick':
|
|
return 3;
|
|
case 'optional-button':
|
|
return 4;
|
|
case 'optional-thumbstick':
|
|
return 5;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
_getAxesStartIndex(buttonType) {
|
|
switch (buttonType) {
|
|
case 'touchpad':
|
|
return 0;
|
|
case 'thumbstick':
|
|
return 2;
|
|
case 'optional-thumbstick':
|
|
return 4;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
_getMojoFromInputSource(mojo_from_viewer) {
|
|
return this.mojo_from_pointer_.matrix;
|
|
}
|
|
}
|
|
|
|
// Mojo helper classes
|
|
class FakeXRHitTestSourceController {
|
|
constructor(id) {
|
|
this.id_ = id;
|
|
this.deleted_ = false;
|
|
}
|
|
|
|
get deleted() {
|
|
return this.deleted_;
|
|
}
|
|
|
|
// Internal setter:
|
|
set deleted(value) {
|
|
this.deleted_ = value;
|
|
}
|
|
}
|
|
|
|
class MockXRPresentationProvider {
|
|
constructor() {
|
|
this.receiver_ = null;
|
|
this.submit_frame_count_ = 0;
|
|
this.missing_frame_count_ = 0;
|
|
}
|
|
|
|
_bindProvider() {
|
|
const provider = new vrMojom.XRPresentationProviderRemote();
|
|
|
|
if (this.receiver_) {
|
|
this.receiver_.$.close();
|
|
}
|
|
this.receiver_ = new vrMojom.XRPresentationProviderReceiver(this);
|
|
this.receiver_.$.bindHandle(provider.$.bindNewPipeAndPassReceiver().handle);
|
|
return provider;
|
|
}
|
|
|
|
_getClientReceiver() {
|
|
this.submitFrameClient_ = new vrMojom.XRPresentationClientRemote();
|
|
return this.submitFrameClient_.$.bindNewPipeAndPassReceiver();
|
|
}
|
|
|
|
// XRPresentationProvider mojo implementation
|
|
updateLayerBounds(frameId, leftBounds, rightBounds, sourceSize) {}
|
|
|
|
submitFrameMissing(frameId, mailboxHolder, timeWaited) {
|
|
this.missing_frame_count_++;
|
|
}
|
|
|
|
submitFrame(frameId, mailboxHolder, timeWaited) {
|
|
this.submit_frame_count_++;
|
|
|
|
// Trigger the submit completion callbacks here. WARNING: The
|
|
// Javascript-based mojo mocks are *not* re-entrant. It's OK to
|
|
// wait for these notifications on the next frame, but waiting
|
|
// within the current frame would never finish since the incoming
|
|
// calls would be queued until the current execution context finishes.
|
|
this.submitFrameClient_.onSubmitFrameTransferred(true);
|
|
this.submitFrameClient_.onSubmitFrameRendered();
|
|
}
|
|
|
|
submitFrameWithTextureHandle(frameId, texture, syncToken) {}
|
|
|
|
submitFrameDrawnIntoTexture(frameId, syncToken, timeWaited) {}
|
|
|
|
// Utility methods
|
|
_close() {
|
|
if (this.receiver_) {
|
|
this.receiver_.$.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export these into the global object as a side effect of importing this
|
|
// module.
|
|
self.ChromeXRTest = ChromeXRTest;
|
|
self.MockRuntime = MockRuntime;
|
|
self.MockVRService = MockVRService;
|
|
self.SubscribeToHitTestResult = vrMojom.SubscribeToHitTestResult;
|
|
|
|
navigator.xr.test = new ChromeXRTest();
|