Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 72 additions & 14 deletions ViroRenderer/VROInputControllerBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,41 @@ void VROInputControllerBase::onButtonEvent(int source, VROEventDelegate::ClickSt
std::shared_ptr<VRONode> &lastClicked = sourceAware
? _lastClickedNodesBySource[source]
: _lastClickedNode;
std::shared_ptr<VRONode> &lastHovered = sourceAware
? _lastHoveredNodesBySource[source]
: _lastHoveredNode;
HoverPending &pending = sourceAware
? _hoverPendingBySource[source]
: _hoverPending;

VROVector3f hitLoc = hit->getLocation();
std::vector<float> pos = {hitLoc.x, hitLoc.y, hitLoc.z};
if (hit->isBackgroundHit()) {
pos.clear();
}

// Hover-hysteresis stickiness: if the user pulled the trigger while the
// ray-cast was momentarily jittered off the target — i.e. a hover
// pending-exit window is still open and the current hit does not land
// on `lastHovered` — re-route the click to `lastHovered`. From the
// user's perspective they are still pointing at the last hovered
// node; the raycast just blinked off for 1–3 frames. Without this,
// the trigger pull resolved against background and JS callers had to
// press "dozens of times" to land a single click.
std::shared_ptr<VRONode> hitNode = hit->getNode();
if (lastHovered != nullptr &&
hitNode != lastHovered &&
pending.candidateNode != nullptr &&
pending.startedMillis >= 0 &&
(VROTimeCurrentMillis() - pending.startedMillis) < kHoverHysteresisMillis) {
hitNode = lastHovered;
pos.clear(); // we don't retain the on-target hit position; payload
// shape matches a background hit. Handlers usually
// care about source / clickState, not position.
}

// Notify internal delegates
std::shared_ptr<VRONode> focusedNode = getNodeToHandleEvent(VROEventDelegate::EventAction::OnClick, hit->getNode());
std::shared_ptr<VRONode> focusedNode = getNodeToHandleEvent(VROEventDelegate::EventAction::OnClick, hitNode);
for (std::shared_ptr<VROEventDelegate> delegate : _delegates) {
delegate->onClick(source, focusedNode, clickState, pos);
}
Expand All @@ -113,7 +139,7 @@ void VROInputControllerBase::onButtonEvent(int source, VROEventDelegate::ClickSt
given Node and source, trigger an onClicked event.
*/
if (clickState == VROEventDelegate::ClickUp) {
if (hit->getNode() == lastClicked) {
if (hitNode == lastClicked) {
for (std::shared_ptr<VROEventDelegate> delegate : _delegates){
delegate->onClick(source, focusedNode, VROEventDelegate::ClickState::Clicked, pos);
}
Expand All @@ -130,12 +156,12 @@ void VROInputControllerBase::onButtonEvent(int source, VROEventDelegate::ClickSt
}
_lastDraggedNode = nullptr;
} else if (clickState == VROEventDelegate::ClickDown){
lastClicked = hit->getNode();
lastClicked = hitNode;

// Identify if object is draggable.
std::shared_ptr<VRONode> draggableNode
= getNodeToHandleEvent(VROEventDelegate::EventAction::OnDrag,
hit->getNode());
hitNode);

if (draggableNode == nullptr){
return;
Expand Down Expand Up @@ -490,38 +516,70 @@ void VROInputControllerBase::processGazeEvent(int source) {
std::shared_ptr<VRONode> &lastHovered = sourceAware
? _lastHoveredNodesBySource[source]
: _lastHoveredNode;
HoverPending &pending = sourceAware
? _hoverPendingBySource[source]
: _hoverPending;

std::shared_ptr<VRONode> newNode = getNodeToHandleEvent(VROEventDelegate::EventAction::OnHover,
hit->getNode());
for (std::shared_ptr<VROEventDelegate> delegate : _delegates) {
delegate->onGazeHit(source, newNode, *hit.get());
}

// Hysteresis: if the hit-test result returns to the currently-hovered
// node, cancel any in-flight pending exit and emit nothing — the user
// never actually saw a transition.
if (lastHovered == newNode) {
pending = HoverPending{};
return;
}

VROVector3f hitLoc = hit->getLocation();
std::vector<float> pos = {hitLoc.x, hitLoc.y, hitLoc.z};
if (hit->isBackgroundHit()) {
bool isBgHit = hit->isBackgroundHit();
if (isBgHit) {
pos.clear();
}

if (newNode && newNode->getEventDelegate()) {
std::shared_ptr<VROEventDelegate> delegate = newNode->getEventDelegate();
if (delegate) {
delegate->onHover(source, newNode, true, pos);
// First-ever hover into a node (no prior hovered) — fire enter immediately.
// No exit to defer, so no point holding it in pending.
if (lastHovered == nullptr) {
if (newNode && newNode->getEventDelegate()) {
newNode->getEventDelegate()->onHover(source, newNode, true, pos);
}
lastHovered = newNode;
pending = HoverPending{};
return;
}

if (lastHovered && lastHovered->getEventDelegate()) {
std::shared_ptr<VROEventDelegate> delegate = lastHovered->getEventDelegate();
if (delegate) {
delegate->onHover(source, lastHovered, false, pos);
}
// From here on `lastHovered != nullptr` and `newNode != lastHovered`.
// Record / update the pending candidate. We confirm the change only
// after `kHoverHysteresisMillis` of the new candidate persisting,
// which absorbs the 1–3 frame ray-cast jitter of unsteady aim.
double now = VROTimeCurrentMillis();
if (pending.candidateNode != newNode || pending.startedMillis < 0) {
pending.candidateNode = newNode;
pending.candidatePos = hitLoc;
pending.candidateBgHit = isBgHit;
pending.startedMillis = now;
return;
}
if (now - pending.startedMillis < kHoverHysteresisMillis) {
// Same candidate as before, but window not elapsed — keep waiting.
pending.candidatePos = hitLoc;
pending.candidateBgHit = isBgHit;
return;
}

// Window elapsed and candidate held — confirm the transition.
if (newNode && newNode->getEventDelegate()) {
newNode->getEventDelegate()->onHover(source, newNode, true, pos);
}
if (lastHovered && lastHovered->getEventDelegate()) {
lastHovered->getEventDelegate()->onHover(source, lastHovered, false, pos);
}
lastHovered = newNode;
pending = HoverPending{};
}

void VROInputControllerBase::processOnFuseEvent(int source, std::shared_ptr<VRONode> newNode) {
Expand Down
36 changes: 36 additions & 0 deletions ViroRenderer/VROInputControllerBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,42 @@ class VROInputControllerBase {
*/
std::map<int, std::shared_ptr<VRONode>> _lastHoveredNodesBySource;

/*
Hover hysteresis state, per source.

OpenXR controller / hand aim ray-casts naturally jitter — pointing at a
small target with a controller held by an unsteady hand causes the
hit-test to alternate between the target node and the surrounding
background every 1–3 frames. Without hysteresis, every alternation
produced an `onHover(false)` / `onHover(true)` pair to JS, which
manifested as a flickering hover state and "the click takes dozens of
presses to register" — because if the user pulled the trigger on a
"miss" frame, `onButtonEvent` resolved the click against the background
instead of the target.

The hysteresis works as a grace period: when the hit changes from
`lastHovered` to a different node, we do NOT immediately fire the
exit — instead we record the candidate change and the timestamp.
If, within `kHoverHysteresisMillis`, the hit returns to `lastHovered`,
the exit is cancelled (no `onHover(false)` ever fires). If the new
candidate persists past the window, the exit is confirmed and the
normal enter/exit dispatch happens.

The same window is consulted by `onButtonEvent` so that clicks landing
on a transient miss-frame are still routed to `lastHovered` if the
pending-exit window is still open. That eliminates the "many presses
to click" symptom even though the underlying ray-cast still oscillates.
*/
static constexpr double kHoverHysteresisMillis = 75.0;
struct HoverPending {
std::shared_ptr<VRONode> candidateNode; // node the hit currently resolves to
VROVector3f candidatePos; // hit location at the moment of pending start
bool candidateBgHit = false; // whether candidate is a background hit
double startedMillis = -1.0; // wall-clock time when the candidate first appeared
};
std::map<int, HoverPending> _hoverPendingBySource;
HoverPending _hoverPending; // legacy single-source fallback

/*
Returns the first node that is able to handle the event action by bubbling it up.
If nothing is able to handle the event, nullptr is returned.
Expand Down
10 changes: 10 additions & 0 deletions ios/Podfile.visionos
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
platform :visionos, '1.0'
workspace 'ViroRenderer'

# No ARCore, no GVRAudioSDK — all iOS-only.
# ViroKit visionOS uses Metal + CompositorServices directly.
# This intentionally empty pod target gives us clean CocoaPods integration
# (xcconfig stubs, PODS_ROOT, etc.) without any iOS-only framework references.
target 'ViroKit' do
use_frameworks!
end
197 changes: 197 additions & 0 deletions ios/ViroKit-visionOS.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// ViroKit-visionOS.xcconfig
// Build settings applied by build_visionos.sh when archiving for xros / xrsimulator.
//
// Strategy: exclude iOS-only source files from the xros compilation unit.
// VRODriverVisionOS (in VisionOS/ subdirectory) provides Metal-only replacements.
// No source files are modified — the exclusions live entirely here.

// ── SDK / deployment ──────────────────────────────────────────────────────────
SDKROOT = xros
SUPPORTED_PLATFORMS = xros xrsimulator
XROS_DEPLOYMENT_TARGET = 1.0
TARGETED_DEVICE_FAMILY = 7

// ── Metal-only path ───────────────────────────────────────────────────────────
// VRODefines.h defaults VRO_METAL=0; we force it on for visionOS.
OTHER_CFLAGS = $(inherited) -DVRO_METAL=1

// ── Linker ────────────────────────────────────────────────────────────────────
// The Xcode project hard-links -lGVRAudioSDK (iOS-only pod).
// Override for xros to avoid "library not found" linker errors.
OTHER_LDFLAGS[sdk=xros*] =

// Library search paths for xros: the project only defines iphoneos*/iphonesimulator*
// variants, so nothing is inherited for xros. Point to the arm64 static libs.
// These were compiled for iOS arm64 but contain only ARM64 object code and link fine
// for visionOS (same ISA). We add -Wl,-w to suppress platform-mismatch warnings.
LIBRARY_SEARCH_PATHS[sdk=xros*] = $(inherited) "$(PROJECT_DIR)/Libraries/freetype/armv7_arm64" "$(PROJECT_DIR)/Libraries/protobuf/armv7_arm64" "$(PROJECT_DIR)/Libraries/bullet/armv7_arm64" "$(PROJECT_DIR)/Libraries/harfbuzz/armv7_arm64" "$(PROJECT_DIR)/Libraries/reactvisioncca/arm64"

// Stub frameworks for iOS-only system frameworks that appear in the Frameworks
// build phase but are unavailable on visionOS. The stubs export zero symbols so
// the linker is satisfied; no runtime dependency is added.
FRAMEWORK_SEARCH_PATHS[sdk=xros*] = $(inherited) "$(SRCROOT)/Stubs"

// GoogleUtilities (pulled in by ARCore) has functions that don't return on visionOS
// code paths because it checks TARGET_OS_IOS/TVOS/OSX but not TARGET_OS_VISION.
// ARCore is iOS-only and unused on xros — suppress the error so the build proceeds.
OTHER_CFLAGS[sdk=xros*] = $(inherited) -Wno-return-type

// ── Header search paths ───────────────────────────────────────────────────────
// ViroKit/ files include each other by basename (e.g. "VROAudioPlayeriOS.h"),
// so the ViroKit source directory must be in the search path.
HEADER_SEARCH_PATHS = $(inherited) "$(SRCROOT)/ViroKit" "$(SRCROOT)/ViroKit/VisionOS"

// ── Excluded iOS-only sources ─────────────────────────────────────────────────
// These files use OpenGL ES, GLKit, AVFoundation camera, or iOS ARKit APIs
// that do not exist on visionOS. VRODriverVisionOS provides Metal replacements.
//
// Format: space-separated basenames (or glob patterns) — no directory prefix.

EXCLUDED_SOURCE_FILE_NAMES[sdk=xros*] = \
VRODisplayOpenGLiOS.h \
VRODisplayOpenGLiOS.cpp \
VRODisplayOpenGLGVR.h \
VRODriverOpenGLiOS.h \
VRODriverOpenGLiOS.cpp \
VROVideoTextureCacheOpenGL.h \
VROVideoTextureCacheOpenGL.cpp \
VRODistortionRenderer.h \
VRODistortionRenderer.cpp \
VROViewScene.h \
VROViewScene.mm \
VROViewAR.h \
VROViewAR.mm \
VROViewMetal.h \
VROViewMetal.m \
VROViewRecorder.h \
VROViewRecorder.mm \
VROAVCaptureController.h \
VROAVCaptureController.cpp \
VROCameraTextureiOS.h \
VROCameraTextureiOS.cpp \
VROObjectRecognizeriOS.h \
VROObjectRecognizeriOS.cpp \
VROBodyTrackerYolo.h \
VROBodyTrackerYolo.cpp \
VROMonocularDepthEstimator.h \
VROMonocularDepthEstimator.mm \
VROARSessioniOS.h \
VROARSessioniOS.cpp \
VROARCameraiOS.h \
VROARCameraiOS.cpp \
VROARFrameiOS.h \
VROARFrameiOS.cpp \
VROARHitTestResultiOS.h \
VROARHitTestResultiOS.mm \
VROARImageTargetiOS.h \
VROARImageTargetiOS.cpp \
VROARObjectTargetiOS.h \
VROARObjectTargetiOS.cpp \
VROARAnchoriOS.h \
VROARAnchoriOS.cpp \
VROARCameraInertial.h \
VROARCameraInertial.cpp \
VROARFrameInertial.h \
VROARFrameInertial.cpp \
VROVideoTextureiOS.h \
VROVideoTextureiOS.cpp \
VROAudioPlayeriOS.h \
VROAudioPlayeriOS.cpp \
VROSoundDelegateiOS.h \
VROTypefaceiOS.h \
VROTypefaceiOS.cpp \
VROAnimBodyDataiOS.h \
VROAnimBodyDataiOS.cpp \
VROARNodeDelegateiOS.h \
VROARSceneDelegateiOS.h \
VROARCameraPrerecorded.h \
VROARCameraPrerecorded.cpp \
VROApiKeyValidatorDynamo.h \
VROApiKeyValidatorDynamo.m \
VROApiKeyMetrics.h \
VROApiKeyMetrics.m \
VROApiMetricsIncrementRequest.h \
VROApiMetricsIncrementRequest.m \
VROApiMetricsRequest.h \
VROApiMetricsRequest.m \
VROWeakProxy.h \
VROWeakProxy.mm \
VROHeadTracker.h \
VROHeadTracker.cpp \
OrientationEKF.h \
OrientationEKF.mm \
SO3Util.h \
SO3Util.mm \
Matrix3x3d.h \
Matrix3x3d.mm \
Vector3d.h \
Vector3d.mm \
VROBodyIKController.h \
VROBodyIKController.cpp \
VROBodyTrackerController.h \
VROBodyTrackerController.cpp \
VROBodyPlayeriOS.h \
VROBodyTrackerTest.h \
VROBodyTrackerTest.cpp \
VROObjectRecognitionTest.h \
VROObjectRecognitionTest.cpp \
VROBodyRecognitionTest.h \
VROBodyRecognitionTest.cpp \
VROBodyMesherTest.h \
VROBodyMesherTest.cpp \
VROTextTest.h \
VROTextTest.cpp \
VROARPlaneTest.h \
VROARPlaneTest.cpp \
VROAnimatedTextureOpenGL.h \
VROAnimatedTextureOpenGL.cpp \
VRODisplayOpenGL.h \
VRODriverOpenGL.h \
VRODriverOpenGL.cpp \
VROGeometrySubstrateOpenGL.h \
VROGeometrySubstrateOpenGL.cpp \
VROGlyphAtlasOpenGL.h \
VROGlyphAtlasOpenGL.cpp \
VROGlyphOpenGL.h \
VROGlyphOpenGL.cpp \
VROImagePostProcessOpenGL.h \
VROImagePostProcessOpenGL.cpp \
VROMaterialSubstrateOpenGL.h \
VROMaterialSubstrateOpenGL.cpp \
VRORenderTargetOpenGL.h \
VRORenderTargetOpenGL.cpp \
VROTextureSubstrateOpenGL.h \
VROTextureSubstrateOpenGL.cpp \
VROVertexBufferOpenGL.h \
VROVertexBufferOpenGL.cpp \
VROShaderProgram.h \
VROShaderProgram.cpp \
VROShaderFactory.h \
VROShaderFactory.cpp \
VROLightingUBO.h \
VROLightingUBO.cpp \
VROBoneUBO.h \
VROBoneUBO.cpp \
VROParticleUBO.h \
VROParticleUBO.cpp \
VROMaterialShaderBinding.h \
VROMaterialShaderBinding.cpp \
VROVisionEngine.h \
VROVisionEngine.cpp \
VROSoundGVR.h \
VROSoundGVR.cpp \
VROScreen.h \
VROScreen.cpp \
VROSkeletonRenderer.h \
VROSkeletonRenderer.cpp \
VROBodySurfaceRenderer.h \
VROBodySurfaceRenderer.cpp \
VROImageShaderProgram.h \
VROImageShaderProgram.cpp \
VROGVRUtil.h \
VROGVRUtil.cpp \
VROGeometrySubstrateMetal.cpp \
VROCloudAnchorProviderReactVision.h \
VROCloudAnchorProviderReactVision.mm \
VROCloudAnchorProviderARCore.h \
VROCloudAnchorProviderARCore.mm
Loading
Loading