/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/XRInputSource.h" #include "mozilla/dom/XRInputSourceEvent.h" #include "XRNativeOriginViewer.h" #include "XRNativeOriginTracker.h" #include "XRInputSpace.h" #include "VRDisplayClient.h" #include "mozilla/dom/Gamepad.h" #include "mozilla/dom/GamepadManager.h" namespace mozilla::dom { NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(XRInputSource, mParent, mTargetRaySpace, mGripSpace, mGamepad) // Follow the controller profile ids from // https://github.com/immersive-web/webxr-input-profiles. nsTArray GetInputSourceProfile(gfx::VRControllerType aType) { nsTArray profile; nsString id; switch (aType) { case gfx::VRControllerType::HTCVive: id.AssignLiteral("htc-vive"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-touchpad"); profile.AppendElement(id); break; case gfx::VRControllerType::HTCViveCosmos: id.AssignLiteral("htc-vive-cosmos"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-thumbstick"); profile.AppendElement(id); break; case gfx::VRControllerType::HTCViveFocus: id.AssignLiteral("htc-vive-focus"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-touchpad"); profile.AppendElement(id); break; case gfx::VRControllerType::HTCViveFocusPlus: id.AssignLiteral("htc-vive-focus-plus"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-touchpad"); profile.AppendElement(id); break; case gfx::VRControllerType::MSMR: id.AssignLiteral("microsoft-mixed-reality"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-touchpad-thumbstick"); profile.AppendElement(id); break; case gfx::VRControllerType::ValveIndex: id.AssignLiteral("valve-index"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-touchpad-thumbstick"); profile.AppendElement(id); break; case gfx::VRControllerType::OculusGo: id.AssignLiteral("oculus-go"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-touchpad"); profile.AppendElement(id); break; case gfx::VRControllerType::OculusTouch: id.AssignLiteral("oculus-touch"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-thumbstick"); profile.AppendElement(id); break; case gfx::VRControllerType::OculusTouch2: id.AssignLiteral("oculus-touch-v2"); profile.AppendElement(id); id.AssignLiteral("oculus-touch"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-thumbstick"); profile.AppendElement(id); break; case gfx::VRControllerType::OculusTouch3: id.AssignLiteral("oculus-touch-v3"); profile.AppendElement(id); id.AssignLiteral("oculus-touch-v2"); profile.AppendElement(id); id.AssignLiteral("oculus-touch"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-thumbstick"); profile.AppendElement(id); break; case gfx::VRControllerType::PicoGaze: id.AssignLiteral("pico-gaze"); profile.AppendElement(id); id.AssignLiteral("generic-button"); profile.AppendElement(id); break; case gfx::VRControllerType::PicoG2: id.AssignLiteral("pico-g2"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-touchpad"); profile.AppendElement(id); break; case gfx::VRControllerType::PicoNeo2: id.AssignLiteral("pico-neo2"); profile.AppendElement(id); id.AssignLiteral("generic-trigger-squeeze-thumbstick"); profile.AppendElement(id); break; default: NS_WARNING("Unsupported XR input source profile.\n"); break; } return profile; } XRInputSource::XRInputSource(nsISupports* aParent) : mParent(aParent), mGamepad(nullptr), mIndex(-1), mSelectAction(ActionState::ActionState_Released), mSqueezeAction(ActionState::ActionState_Released) {} XRInputSource::~XRInputSource() { mTargetRaySpace = nullptr; mGripSpace = nullptr; mGamepad = nullptr; } JSObject* XRInputSource::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return XRInputSource_Binding::Wrap(aCx, this, aGivenProto); } XRHandedness XRInputSource::Handedness() { return mHandedness; } XRTargetRayMode XRInputSource::TargetRayMode() { return mTargetRayMode; } XRSpace* XRInputSource::TargetRaySpace() { return mTargetRaySpace; } XRSpace* XRInputSource::GetGripSpace() { return mGripSpace; } void XRInputSource::GetProfiles(nsTArray& aResult) { aResult = mProfiles.Clone(); } Gamepad* XRInputSource::GetGamepad() { return mGamepad; } void XRInputSource::Setup(XRSession* aSession, uint32_t aIndex) { MOZ_ASSERT(aSession); gfx::VRDisplayClient* displayClient = aSession->GetDisplayClient(); if (!displayClient) { MOZ_ASSERT(displayClient); return; } const gfx::VRDisplayInfo& displayInfo = displayClient->GetDisplayInfo(); const gfx::VRControllerState& controllerState = displayInfo.mControllerState[aIndex]; MOZ_ASSERT(controllerState.controllerName[0] != '\0'); mProfiles = GetInputSourceProfile(controllerState.type); mHandedness = XRHandedness::None; switch (controllerState.hand) { case GamepadHand::_empty: mHandedness = XRHandedness::None; break; case GamepadHand::Left: mHandedness = XRHandedness::Left; break; case GamepadHand::Right: mHandedness = XRHandedness::Right; break; default: MOZ_ASSERT(false && "Unknown GamepadHand type."); break; } RefPtr nativeOriginTargetRay = nullptr; mTargetRayMode = XRTargetRayMode::Tracked_pointer; switch (controllerState.targetRayMode) { case gfx::TargetRayMode::Gaze: mTargetRayMode = XRTargetRayMode::Gaze; nativeOriginTargetRay = new XRNativeOriginViewer(displayClient); break; case gfx::TargetRayMode::TrackedPointer: mTargetRayMode = XRTargetRayMode::Tracked_pointer; // We use weak pointers of poses in XRNativeOriginTracker to sync their // data internally. nativeOriginTargetRay = new XRNativeOriginTracker(&controllerState.targetRayPose); break; case gfx::TargetRayMode::Screen: mTargetRayMode = XRTargetRayMode::Screen; break; default: MOZ_ASSERT(false && "Undefined TargetRayMode type."); break; } mTargetRaySpace = new XRInputSpace(aSession->GetParentObject(), aSession, nativeOriginTargetRay, aIndex); const uint32_t gamepadHandleValue = displayInfo.mDisplayID * gfx::kVRControllerMaxCount + aIndex; const GamepadHandle gamepadHandle{gamepadHandleValue, GamepadHandleKind::VR}; mGamepad = new Gamepad(mParent, NS_ConvertASCIItoUTF16(""), -1, gamepadHandle, GamepadMappingType::Xr_standard, controllerState.hand, displayInfo.mDisplayID, controllerState.numButtons, controllerState.numAxes, controllerState.numHaptics, 0, 0); mIndex = aIndex; if (!mGripSpace) { CreateGripSpace(aSession, controllerState); } } void XRInputSource::SetGamepadIsConnected(bool aConnected, XRSession* aSession) { mGamepad->SetConnected(aConnected); MOZ_ASSERT(aSession); if (!aConnected) { if (mSelectAction != ActionState::ActionState_Released) { DispatchEvent(u"selectend"_ns, aSession); mSelectAction = ActionState::ActionState_Released; } if (mSqueezeAction != ActionState::ActionState_Released) { DispatchEvent(u"squeezeend"_ns, aSession); mSqueezeAction = ActionState::ActionState_Released; } } } void XRInputSource::Update(XRSession* aSession) { MOZ_ASSERT(aSession && mIndex >= 0 && mGamepad); gfx::VRDisplayClient* displayClient = aSession->GetDisplayClient(); if (!displayClient) { MOZ_ASSERT(displayClient); return; } const gfx::VRDisplayInfo& displayInfo = displayClient->GetDisplayInfo(); const gfx::VRControllerState& controllerState = displayInfo.mControllerState[mIndex]; MOZ_ASSERT(controllerState.controllerName[0] != '\0'); // OculusVR and OpenVR controllers need to wait until // update functions to assign GamepadCapabilityFlags::Cap_GripSpacePosition // flag. if (!mGripSpace) { CreateGripSpace(aSession, controllerState); } // Update button values. nsTArray> buttons; mGamepad->GetButtons(buttons); for (uint32_t i = 0; i < buttons.Length(); ++i) { const bool pressed = controllerState.buttonPressed & (1ULL << i); const bool touched = controllerState.buttonTouched & (1ULL << i); if (buttons[i]->Pressed() != pressed || buttons[i]->Touched() != touched || buttons[i]->Value() != controllerState.triggerValue[i]) { mGamepad->SetButton(i, pressed, touched, controllerState.triggerValue[i]); } } // Update axis values. nsTArray axes; mGamepad->GetAxes(axes); for (uint32_t i = 0; i < axes.Length(); ++i) { if (axes[i] != controllerState.axisValue[i]) { mGamepad->SetAxis(i, controllerState.axisValue[i]); } } // We define 0.85f and 0.15f based on our current finding // for better experience, we can adjust these values if we need. const float completeThreshold = 0.90f; const float startThreshold = 0.85f; const float endThreshold = 0.15f; const uint32_t selectIndex = 0; const uint32_t squeezeIndex = 1; // Checking selectstart, select, selectend if (buttons.Length() > selectIndex) { if (controllerState.selectActionStartFrameId > controllerState.selectActionStopFrameId) { if (mSelectAction == ActionState::ActionState_Released && controllerState.triggerValue[selectIndex] > endThreshold) { DispatchEvent(u"selectstart"_ns, aSession); mSelectAction = ActionState::ActionState_Pressing; } else if (mSelectAction == ActionState::ActionState_Pressing && controllerState.triggerValue[selectIndex] > completeThreshold) { mSelectAction = ActionState::ActionState_Pressed; } else if (mSelectAction == ActionState::ActionState_Pressed && controllerState.triggerValue[selectIndex] < startThreshold) { DispatchEvent(u"select"_ns, aSession); mSelectAction = ActionState::ActionState_Releasing; } else if (mSelectAction <= ActionState::ActionState_Releasing && controllerState.triggerValue[selectIndex] < endThreshold) { // For a select btn which only has pressed and unpressed status. if (mSelectAction == ActionState::ActionState_Pressed) { DispatchEvent(u"select"_ns, aSession); } DispatchEvent(u"selectend"_ns, aSession); mSelectAction = ActionState::ActionState_Released; } } else if (mSelectAction <= ActionState::ActionState_Releasing) { // For a select btn which only has pressed and unpressed status. if (mSelectAction == ActionState::ActionState_Pressed) { DispatchEvent(u"select"_ns, aSession); } DispatchEvent(u"selectend"_ns, aSession); mSelectAction = ActionState::ActionState_Released; } } // Checking squeezestart, squeeze, squeezeend if (buttons.Length() > squeezeIndex) { if (controllerState.squeezeActionStartFrameId > controllerState.squeezeActionStopFrameId) { if (mSqueezeAction == ActionState::ActionState_Released && controllerState.triggerValue[squeezeIndex] > endThreshold) { DispatchEvent(u"squeezestart"_ns, aSession); mSqueezeAction = ActionState::ActionState_Pressing; } else if (mSqueezeAction == ActionState::ActionState_Pressing && controllerState.triggerValue[squeezeIndex] > completeThreshold) { mSqueezeAction = ActionState::ActionState_Pressed; } else if (mSqueezeAction == ActionState::ActionState_Pressed && controllerState.triggerValue[squeezeIndex] < startThreshold) { DispatchEvent(u"squeeze"_ns, aSession); mSqueezeAction = ActionState::ActionState_Releasing; } else if (mSqueezeAction <= ActionState::ActionState_Releasing && controllerState.triggerValue[squeezeIndex] < endThreshold) { // For a squeeze btn which only has pressed and unpressed status. if (mSqueezeAction == ActionState::ActionState_Pressed) { DispatchEvent(u"squeeze"_ns, aSession); } DispatchEvent(u"squeezeend"_ns, aSession); mSqueezeAction = ActionState::ActionState_Released; } } else if (mSqueezeAction <= ActionState::ActionState_Releasing) { // For a squeeze btn which only has pressed and unpressed status. if (mSqueezeAction == ActionState::ActionState_Pressed) { DispatchEvent(u"squeeze"_ns, aSession); } DispatchEvent(u"squeezeend"_ns, aSession); mSqueezeAction = ActionState::ActionState_Released; } } } int32_t XRInputSource::GetIndex() { return mIndex; } void XRInputSource::DispatchEvent(const nsAString& aEvent, XRSession* aSession) { if (!GetParentObject() || !aSession) { return; } // Create a XRFrame for its callbacks RefPtr frame = new XRFrame(GetParentObject(), aSession); frame->StartInputSourceEvent(); XRInputSourceEventInit init; init.mBubbles = false; init.mCancelable = false; init.mFrame = frame; init.mInputSource = this; RefPtr event = XRInputSourceEvent::Constructor(aSession, aEvent, init); event->SetTrusted(true); aSession->DispatchEvent(*event); frame->EndInputSourceEvent(); } void XRInputSource::CreateGripSpace( XRSession* aSession, const gfx::VRControllerState& controllerState) { MOZ_ASSERT(!mGripSpace); MOZ_ASSERT(aSession && mIndex >= 0 && mGamepad); if (mTargetRayMode == XRTargetRayMode::Tracked_pointer && controllerState.flags & GamepadCapabilityFlags::Cap_GripSpacePosition) { RefPtr nativeOriginGrip = nullptr; nativeOriginGrip = new XRNativeOriginTracker(&controllerState.pose); mGripSpace = new XRInputSpace(aSession->GetParentObject(), aSession, nativeOriginGrip, mIndex); } else { mGripSpace = nullptr; } } } // namespace mozilla::dom