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
9 changes: 9 additions & 0 deletions samples/gestures_heuristic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Heuristic Gesture HUD

This sample enables the heuristic gesture recognizer and shows a simple HUD that
displays the most confident gesture per hand while logging gesture start/end
events to the console. Use it to validate pinch, open-palm, fist, thumbs-up,
point, and spread detections in the simulator or on device.

Run `npm run serve` (or your preferred static server) and open
`samples/gestures_heuristic/` in a WebXR-capable browser.
42 changes: 42 additions & 0 deletions samples/gestures_heuristic/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<title>Gestures: Heuristic HUD</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link type="text/css" rel="stylesheet" href="../main.css" />
<script>
window.litDisableBundleWarning = true;
</script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/",
"troika-three-text": "https://cdn.jsdelivr.net/gh/protectwise/troika@028b81cf308f0f22e5aa8e78196be56ec1997af5/packages/troika-three-text/src/index.js",
"troika-three-utils": "https://cdn.jsdelivr.net/gh/protectwise/[email protected]/packages/troika-three-utils/src/index.js",
"troika-worker-utils": "https://cdn.jsdelivr.net/gh/protectwise/[email protected]/packages/troika-worker-utils/src/index.js",
"bidi-js": "https://esm.sh/bidi-js@%5E1.0.2?target=es2022",
"webgl-sdf-generator": "https://esm.sh/[email protected]/es2022/webgl-sdf-generator.mjs",
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js",
"lit/": "https://esm.run/lit@3/",
"xrblocks": "../../build/xrblocks.js",
"xrblocks/addons/": "../../build/addons/"
}
}
</script>
</head>

<body>
<script type="module" src="main.js"></script>
</body>
</html>
212 changes: 212 additions & 0 deletions samples/gestures_heuristic/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import 'xrblocks/addons/simulator/SimulatorAddons.js';

import * as xb from 'xrblocks';

const options = new xb.Options();
options.enableReticles();
options.enableGestures();

options.gestures.setGestureEnabled('point', true);
options.gestures.setGestureEnabled('spread', true);

options.hands.enabled = true;
options.hands.visualization = true;
options.hands.visualizeJoints = true;
options.hands.visualizeMeshes = true;

options.simulator.defaultMode = xb.SimulatorMode.POSE;

function createHudElement() {
const style = document.createElement('style');
style.textContent = `
#gesture-hud {
position: fixed;
top: 12px;
right: 12px;
min-width: 220px;
padding: 12px;
border-radius: 12px;
background: rgba(10, 12, 20, 0.82);
color: #f4f4f4;
font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
line-height: 1.4;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.3);
z-index: 9999;
}
#gesture-hud h2 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.01em;
}
#gesture-hud .hand-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.04);
margin-bottom: 6px;
}
#gesture-hud .hand-row:last-child {
margin-bottom: 0;
}
#gesture-hud .hand-label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 11px;
opacity: 0.75;
}
#gesture-hud .gesture {
font-weight: 700;
}
#gesture-hud .gesture[data-active="false"] {
opacity: 0.65;
}
`;
document.head.appendChild(style);

const container = document.createElement('div');
container.id = 'gesture-hud';
container.innerHTML = `
<h2>Gestures</h2>
<div class="hand-row">
<span class="hand-label">Left</span>
<span class="gesture" data-hand="left" data-active="false">None</span>
</div>
<div class="hand-row">
<span class="hand-label">Right</span>
<span class="gesture" data-hand="right" data-active="false">None</span>
</div>
`;
document.body.appendChild(container);
return container;
}

class GestureLogger extends xb.Script {
init() {
const gestures = xb.core.gestureRecognition;
if (!gestures) {
console.warn(
'[GestureLogger] GestureRecognition is unavailable. ' +
'Make sure options.enableGestures() is called before xb.init().'
);
return;
}
this._onGestureStart = (event) => {
const {hand, name, confidence = 0} = event.detail;
console.log(
`[gesture] ${hand} hand started ${name} (${confidence.toFixed(2)})`
);
};
this._onGestureEnd = (event) => {
const {hand, name} = event.detail;
console.log(`[gesture] ${hand} hand ended ${name}`);
};
gestures.addEventListener('gesturestart', this._onGestureStart);
gestures.addEventListener('gestureend', this._onGestureEnd);
}

dispose() {
const gestures = xb.core.gestureRecognition;
if (!gestures) return;
if (this._onGestureStart) {
gestures.removeEventListener('gesturestart', this._onGestureStart);
}
if (this._onGestureEnd) {
gestures.removeEventListener('gestureend', this._onGestureEnd);
}
}
}

class GestureHUD extends xb.Script {
init() {
this._container = createHudElement();
this._active = {
left: new Map(),
right: new Map(),
};
this._labels = {
left: this._container.querySelector('[data-hand="left"]'),
right: this._container.querySelector('[data-hand="right"]'),
};

const gestures = xb.core.gestureRecognition;
if (!gestures) {
console.warn(
'[GestureHUD] GestureRecognition is unavailable. ' +
'Make sure options.enableGestures() is called before xb.init().'
);
return;
}

const update = (event) => {
const {name, hand, confidence = 0} = event.detail;
this._active[hand].set(name, confidence);
this._refresh(hand);
};
const clear = (event) => {
const {name, hand} = event.detail;
this._active[hand].delete(name);
this._refresh(hand);
};

this._onGestureStart = update;
this._onGestureUpdate = update;
this._onGestureEnd = clear;

gestures.addEventListener('gesturestart', this._onGestureStart);
gestures.addEventListener('gestureupdate', this._onGestureUpdate);
gestures.addEventListener('gestureend', this._onGestureEnd);
}

_refresh(hand) {
const label = this._labels[hand];
if (!label) return;
const entries = this._active[hand];
if (!entries || entries.size === 0) {
label.dataset.active = 'false';
label.textContent = 'None';
return;
}
let topGesture = 'None';
let topConfidence = 0;
for (const [name, confidence] of entries.entries()) {
if (confidence >= topConfidence) {
topGesture = name;
topConfidence = confidence;
}
}
label.dataset.active = 'true';
label.textContent = `${topGesture} (${topConfidence.toFixed(2)})`;
}

dispose() {
const gestures = xb.core.gestureRecognition;
if (gestures) {
if (this._onGestureStart) {
gestures.removeEventListener('gesturestart', this._onGestureStart);
}
if (this._onGestureUpdate) {
gestures.removeEventListener('gestureupdate', this._onGestureUpdate);
}
if (this._onGestureEnd) {
gestures.removeEventListener('gestureend', this._onGestureEnd);
}
}
if (this._container?.parentElement) {
this._container.parentElement.removeChild(this._container);
}
}
}

function start() {
xb.add(new GestureLogger());
xb.add(new GestureHUD());
xb.init(options);
}

document.addEventListener('DOMContentLoaded', () => {
start();
});
Loading