webxr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +7 -0
  4. data/README.md +138 -0
  5. data/Rakefile +21 -0
  6. data/examples/ar_demo.html +238 -0
  7. data/examples/ar_hit_test.rb +157 -0
  8. data/examples/basic_vr.rb +110 -0
  9. data/examples/controller_input.rb +91 -0
  10. data/examples/hand_tracking.rb +124 -0
  11. data/examples/hello_webxr.html +288 -0
  12. data/examples/inline_demo.html +261 -0
  13. data/examples/server.rb +34 -0
  14. data/examples/vr_scene_demo.html +330 -0
  15. data/lib/webxr/ar/anchor.rb +83 -0
  16. data/lib/webxr/ar/hit_test_result.rb +54 -0
  17. data/lib/webxr/ar/hit_test_source.rb +34 -0
  18. data/lib/webxr/ar/ray.rb +90 -0
  19. data/lib/webxr/constants.rb +61 -0
  20. data/lib/webxr/core/frame.rb +155 -0
  21. data/lib/webxr/core/render_state.rb +47 -0
  22. data/lib/webxr/core/session.rb +212 -0
  23. data/lib/webxr/core/system.rb +122 -0
  24. data/lib/webxr/errors.rb +18 -0
  25. data/lib/webxr/events/input_source_event.rb +53 -0
  26. data/lib/webxr/events/reference_space_event.rb +44 -0
  27. data/lib/webxr/events/session_event.rb +56 -0
  28. data/lib/webxr/geometry/pose.rb +49 -0
  29. data/lib/webxr/geometry/rigid_transform.rb +73 -0
  30. data/lib/webxr/geometry/view.rb +68 -0
  31. data/lib/webxr/geometry/viewer_pose.rb +40 -0
  32. data/lib/webxr/geometry/viewport.rb +55 -0
  33. data/lib/webxr/hand/hand.rb +197 -0
  34. data/lib/webxr/hand/joint_pose.rb +33 -0
  35. data/lib/webxr/hand/joint_space.rb +74 -0
  36. data/lib/webxr/helpers/input_helper.rb +142 -0
  37. data/lib/webxr/helpers/rendering_helper.rb +94 -0
  38. data/lib/webxr/helpers/session_manager.rb +105 -0
  39. data/lib/webxr/input/gamepad.rb +115 -0
  40. data/lib/webxr/input/gamepad_button.rb +36 -0
  41. data/lib/webxr/input/input_source.rb +101 -0
  42. data/lib/webxr/input/input_source_array.rb +86 -0
  43. data/lib/webxr/js_wrapper.rb +116 -0
  44. data/lib/webxr/layers/layer.rb +28 -0
  45. data/lib/webxr/layers/webgl_binding.rb +69 -0
  46. data/lib/webxr/layers/webgl_layer.rb +102 -0
  47. data/lib/webxr/layers/webgl_sub_image.rb +59 -0
  48. data/lib/webxr/spaces/bounded_reference_space.rb +43 -0
  49. data/lib/webxr/spaces/reference_space.rb +51 -0
  50. data/lib/webxr/spaces/space.rb +18 -0
  51. data/lib/webxr/version.rb +5 -0
  52. data/lib/webxr.rb +73 -0
  53. data/webxr.gemspec +33 -0
  54. metadata +111 -0
@@ -0,0 +1,261 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WebXR Ruby - Inline Demo (No VR Required)</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ background: #1a1a2e;
11
+ color: white;
12
+ font-family: system-ui, sans-serif;
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: center;
16
+ min-height: 100vh;
17
+ padding: 20px;
18
+ }
19
+ h1 { color: #e94560; margin-bottom: 10px; }
20
+ p { color: #aaa; margin-bottom: 20px; }
21
+ #xr-canvas {
22
+ width: 800px;
23
+ height: 450px;
24
+ max-width: 100%;
25
+ border: 2px solid #e94560;
26
+ border-radius: 8px;
27
+ }
28
+ button {
29
+ background: #e94560;
30
+ color: white;
31
+ border: none;
32
+ padding: 1rem 2rem;
33
+ font-size: 1.2rem;
34
+ border-radius: 8px;
35
+ cursor: pointer;
36
+ margin: 1rem 0.5rem;
37
+ transition: background 0.3s;
38
+ }
39
+ button:hover { background: #ff6b6b; }
40
+ button:disabled { background: #444; cursor: not-allowed; }
41
+ #log {
42
+ margin-top: 1rem;
43
+ padding: 1rem;
44
+ background: #0f0f23;
45
+ border-radius: 8px;
46
+ text-align: left;
47
+ max-height: 150px;
48
+ overflow-y: auto;
49
+ font-family: monospace;
50
+ font-size: 0.85rem;
51
+ width: 800px;
52
+ max-width: 100%;
53
+ }
54
+ .log-entry { margin: 0.25rem 0; }
55
+ .info { color: #4ecdc4; }
56
+ .success { color: #7bed9f; }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <h1>WebXR Ruby - Inline Demo</h1>
61
+ <p>This demo works without VR headset or emulator (uses WebXR inline mode)</p>
62
+
63
+ <canvas id="xr-canvas"></canvas>
64
+
65
+ <div>
66
+ <button id="start-btn">Start Inline Session</button>
67
+ <button id="stop-btn" disabled>Stop Session</button>
68
+ </div>
69
+
70
+ <div id="log"></div>
71
+
72
+ <script type="module">
73
+ const canvas = document.getElementById("xr-canvas");
74
+ const startBtn = document.getElementById("start-btn");
75
+ const stopBtn = document.getElementById("stop-btn");
76
+ const logDiv = document.getElementById("log");
77
+
78
+ let currentSession = null;
79
+ let gl = null;
80
+
81
+ function log(message, type = "") {
82
+ const entry = document.createElement("div");
83
+ entry.className = `log-entry ${type}`;
84
+ const time = new Date().toLocaleTimeString();
85
+ entry.textContent = `[${time}] ${message}`;
86
+ logDiv.appendChild(entry);
87
+ logDiv.scrollTop = logDiv.scrollHeight;
88
+ console.log(message);
89
+ }
90
+
91
+ async function init() {
92
+ // Setup WebGL
93
+ gl = canvas.getContext("webgl2", { xrCompatible: true });
94
+ if (!gl) {
95
+ gl = canvas.getContext("webgl", { xrCompatible: true });
96
+ }
97
+ if (!gl) {
98
+ log("WebGL not supported!", "error");
99
+ return;
100
+ }
101
+ log("WebGL context created", "success");
102
+
103
+ // Check WebXR
104
+ if (!navigator.xr) {
105
+ log("WebXR not available - showing regular WebGL demo");
106
+ startRegularDemo();
107
+ return;
108
+ }
109
+
110
+ log("WebXR API detected", "info");
111
+
112
+ // Check inline support (should always be supported)
113
+ try {
114
+ const supported = await navigator.xr.isSessionSupported("inline");
115
+ if (supported) {
116
+ log("Inline session supported", "success");
117
+ startBtn.disabled = false;
118
+ } else {
119
+ log("Inline session not supported - showing regular demo");
120
+ startRegularDemo();
121
+ }
122
+ } catch (e) {
123
+ log("Error checking support: " + e.message);
124
+ startRegularDemo();
125
+ }
126
+ }
127
+
128
+ function startRegularDemo() {
129
+ log("Running regular WebGL animation...", "info");
130
+ startBtn.style.display = "none";
131
+ stopBtn.style.display = "none";
132
+
133
+ let hue = 0;
134
+ function render() {
135
+ hue = (hue + 0.5) % 360;
136
+ const rgb = hslToRgb(hue / 360, 0.6, 0.4);
137
+
138
+ gl.clearColor(rgb[0], rgb[1], rgb[2], 1.0);
139
+ gl.clear(gl.COLOR_BUFFER_BIT);
140
+
141
+ requestAnimationFrame(render);
142
+ }
143
+ render();
144
+ }
145
+
146
+ async function startInlineSession() {
147
+ try {
148
+ log("Starting inline XR session...");
149
+ startBtn.disabled = true;
150
+
151
+ currentSession = await navigator.xr.requestSession("inline");
152
+ log("Inline session started!", "success");
153
+
154
+ stopBtn.disabled = false;
155
+
156
+ // Create XR-compatible layer
157
+ const layer = new XRWebGLLayer(currentSession, gl);
158
+ currentSession.updateRenderState({
159
+ baseLayer: layer,
160
+ inlineVerticalFieldOfView: Math.PI / 2 // 90 degrees
161
+ });
162
+
163
+ log(`Layer: ${layer.framebufferWidth}x${layer.framebufferHeight}`);
164
+
165
+ // Get viewer reference space
166
+ const refSpace = await currentSession.requestReferenceSpace("viewer");
167
+ log("Viewer reference space acquired", "info");
168
+
169
+ // Handle session end
170
+ currentSession.addEventListener("end", () => {
171
+ log("Session ended");
172
+ currentSession = null;
173
+ startBtn.disabled = false;
174
+ stopBtn.disabled = true;
175
+ });
176
+
177
+ // Render loop
178
+ let frameCount = 0;
179
+ let hue = 0;
180
+
181
+ function onFrame(time, frame) {
182
+ if (!currentSession) return;
183
+ currentSession.requestAnimationFrame(onFrame);
184
+ frameCount++;
185
+
186
+ const pose = frame.getViewerPose(refSpace);
187
+ if (!pose) return;
188
+
189
+ // Bind framebuffer (null for inline = draw to canvas)
190
+ gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer);
191
+
192
+ // Animated color
193
+ hue = (hue + 0.3) % 360;
194
+ const rgb = hslToRgb(hue / 360, 0.6, 0.4);
195
+ gl.clearColor(rgb[0], rgb[1], rgb[2], 1.0);
196
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
197
+
198
+ // Process each view
199
+ for (const view of pose.views) {
200
+ const viewport = layer.getViewport(view);
201
+ gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
202
+
203
+ // In a real app, render 3D content using:
204
+ // - view.projectionMatrix
205
+ // - view.transform.matrix
206
+ }
207
+
208
+ if (frameCount % 60 === 0) {
209
+ log(`Frame ${frameCount} - Color: hsl(${Math.round(hue)}, 60%, 40%)`);
210
+ }
211
+ }
212
+
213
+ currentSession.requestAnimationFrame(onFrame);
214
+ log("Render loop started", "success");
215
+
216
+ } catch (e) {
217
+ log("Error: " + e.message);
218
+ startBtn.disabled = false;
219
+ }
220
+ }
221
+
222
+ async function stopSession() {
223
+ if (currentSession) {
224
+ await currentSession.end();
225
+ log("Session stopped");
226
+ }
227
+ }
228
+
229
+ // HSL to RGB
230
+ function hslToRgb(h, s, l) {
231
+ let r, g, b;
232
+ if (s === 0) {
233
+ r = g = b = l;
234
+ } else {
235
+ const hue2rgb = (p, q, t) => {
236
+ if (t < 0) t += 1;
237
+ if (t > 1) t -= 1;
238
+ if (t < 1/6) return p + (q - p) * 6 * t;
239
+ if (t < 1/2) return q;
240
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
241
+ return p;
242
+ };
243
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
244
+ const p = 2 * l - q;
245
+ r = hue2rgb(p, q, h + 1/3);
246
+ g = hue2rgb(p, q, h);
247
+ b = hue2rgb(p, q, h - 1/3);
248
+ }
249
+ return [r, g, b];
250
+ }
251
+
252
+ // Event listeners
253
+ startBtn.addEventListener("click", startInlineSession);
254
+ stopBtn.addEventListener("click", stopSession);
255
+
256
+ // Initialize
257
+ init();
258
+ log("Demo initialized", "info");
259
+ </script>
260
+ </body>
261
+ </html>
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "webrick"
5
+
6
+ port = ENV.fetch("PORT", 8000).to_i
7
+ document_root = __dir__
8
+
9
+ server = WEBrick::HTTPServer.new(
10
+ Port: port,
11
+ DocumentRoot: document_root,
12
+ Logger: WEBrick::Log.new($stdout, WEBrick::Log::INFO),
13
+ AccessLog: [[File.open(File::NULL, "w"), WEBrick::AccessLog::COMMON_LOG_FORMAT]]
14
+ )
15
+
16
+ puts "\n"
17
+ puts "=" * 50
18
+ puts " WebXR Ruby Examples Server"
19
+ puts "=" * 50
20
+ puts "\n"
21
+ puts " Serving files from: #{document_root}"
22
+ puts "\n"
23
+ puts " Open in browser:"
24
+ puts " http://localhost:#{port}/hello_webxr.html"
25
+ puts " http://localhost:#{port}/ar_demo.html"
26
+ puts "\n"
27
+ puts " Press Ctrl+C to stop"
28
+ puts "=" * 50
29
+ puts "\n"
30
+
31
+ trap("INT") { server.shutdown }
32
+ trap("TERM") { server.shutdown }
33
+
34
+ server.start
@@ -0,0 +1,330 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WebXR Ruby - 3D VR Scene Demo</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ background: #000;
11
+ overflow: hidden;
12
+ }
13
+ canvas {
14
+ display: block;
15
+ }
16
+ #info {
17
+ position: absolute;
18
+ top: 10px;
19
+ left: 10px;
20
+ color: white;
21
+ font-family: system-ui, sans-serif;
22
+ font-size: 14px;
23
+ background: rgba(0,0,0,0.7);
24
+ padding: 15px;
25
+ border-radius: 8px;
26
+ z-index: 100;
27
+ max-width: 300px;
28
+ pointer-events: none;
29
+ }
30
+ #info h2 {
31
+ margin: 0 0 10px 0;
32
+ color: #e94560;
33
+ }
34
+ #info p {
35
+ margin: 5px 0;
36
+ color: #ccc;
37
+ }
38
+ #vr-button {
39
+ position: absolute;
40
+ bottom: 20px;
41
+ left: 50%;
42
+ transform: translateX(-50%);
43
+ background: #e94560;
44
+ color: white;
45
+ border: none;
46
+ padding: 15px 30px;
47
+ font-size: 18px;
48
+ border-radius: 8px;
49
+ cursor: pointer;
50
+ z-index: 100;
51
+ }
52
+ #vr-button:hover { background: #ff6b6b; }
53
+ #vr-button:disabled { background: #444; cursor: not-allowed; }
54
+ #log {
55
+ position: absolute;
56
+ bottom: 80px;
57
+ left: 10px;
58
+ color: #4ecdc4;
59
+ font-family: monospace;
60
+ font-size: 12px;
61
+ background: rgba(0,0,0,0.7);
62
+ padding: 10px;
63
+ border-radius: 8px;
64
+ max-height: 150px;
65
+ overflow-y: auto;
66
+ z-index: 100;
67
+ width: 280px;
68
+ pointer-events: none;
69
+ }
70
+ </style>
71
+
72
+ <script type="importmap">
73
+ {
74
+ "imports": {
75
+ "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
76
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
77
+ }
78
+ }
79
+ </script>
80
+ </head>
81
+ <body>
82
+ <div id="info">
83
+ <h2>WebXR 3D Scene</h2>
84
+ <p>Drag to rotate view</p>
85
+ <p>Scroll to zoom</p>
86
+ <p>VR: Install WebXR Emulator or use VR headset</p>
87
+ </div>
88
+
89
+ <button id="vr-button" disabled>Enter VR</button>
90
+
91
+ <div id="log"></div>
92
+
93
+ <script type="module">
94
+ import * as THREE from 'three';
95
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
96
+
97
+ const logDiv = document.getElementById("log");
98
+ const vrButton = document.getElementById("vr-button");
99
+
100
+ function log(msg) {
101
+ const entry = document.createElement("div");
102
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
103
+ logDiv.appendChild(entry);
104
+ logDiv.scrollTop = logDiv.scrollHeight;
105
+ console.log(msg);
106
+ }
107
+
108
+ // Scene setup
109
+ const scene = new THREE.Scene();
110
+
111
+ // Starfield background
112
+ const starGeometry = new THREE.BufferGeometry();
113
+ const starCount = 2000;
114
+ const starPositions = new Float32Array(starCount * 3);
115
+ for (let i = 0; i < starCount * 3; i += 3) {
116
+ starPositions[i] = (Math.random() - 0.5) * 100;
117
+ starPositions[i + 1] = (Math.random() - 0.5) * 100;
118
+ starPositions[i + 2] = (Math.random() - 0.5) * 100;
119
+ }
120
+ starGeometry.setAttribute("position", new THREE.BufferAttribute(starPositions, 3));
121
+ const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.1 });
122
+ const stars = new THREE.Points(starGeometry, starMaterial);
123
+ scene.add(stars);
124
+
125
+ // Floor grid
126
+ const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222);
127
+ gridHelper.position.y = -1;
128
+ scene.add(gridHelper);
129
+
130
+ // Central glowing cube
131
+ const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
132
+ const cubeMaterial = new THREE.MeshStandardMaterial({
133
+ color: 0xe94560,
134
+ emissive: 0xe94560,
135
+ emissiveIntensity: 0.3,
136
+ metalness: 0.8,
137
+ roughness: 0.2
138
+ });
139
+ const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
140
+ cube.position.y = 0.5;
141
+ scene.add(cube);
142
+
143
+ // Floating spheres
144
+ const spheres = [];
145
+ const sphereColors = [0x4ecdc4, 0xffe66d, 0x7bed9f, 0xa55eea, 0x00d2d3];
146
+ for (let i = 0; i < 5; i++) {
147
+ const sphereGeometry = new THREE.SphereGeometry(0.3, 32, 32);
148
+ const sphereMaterial = new THREE.MeshStandardMaterial({
149
+ color: sphereColors[i],
150
+ emissive: sphereColors[i],
151
+ emissiveIntensity: 0.2,
152
+ metalness: 0.5,
153
+ roughness: 0.3
154
+ });
155
+ const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
156
+ sphere.userData = {
157
+ angle: (i / 5) * Math.PI * 2,
158
+ radius: 2 + i * 0.3,
159
+ speed: 0.5 + i * 0.1,
160
+ yOffset: Math.sin(i) * 0.5
161
+ };
162
+ spheres.push(sphere);
163
+ scene.add(sphere);
164
+ }
165
+
166
+ // Torus knot
167
+ const torusGeometry = new THREE.TorusKnotGeometry(0.5, 0.15, 100, 16);
168
+ const torusMaterial = new THREE.MeshStandardMaterial({
169
+ color: 0xff6b6b,
170
+ emissive: 0xff6b6b,
171
+ emissiveIntensity: 0.2,
172
+ metalness: 0.7,
173
+ roughness: 0.3
174
+ });
175
+ const torus = new THREE.Mesh(torusGeometry, torusMaterial);
176
+ torus.position.set(3, 1, -2);
177
+ scene.add(torus);
178
+
179
+ // Lights
180
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
181
+ scene.add(ambientLight);
182
+
183
+ const pointLight = new THREE.PointLight(0xffffff, 1, 50);
184
+ pointLight.position.set(5, 5, 5);
185
+ scene.add(pointLight);
186
+
187
+ const pointLight2 = new THREE.PointLight(0x4ecdc4, 0.5, 30);
188
+ pointLight2.position.set(-5, 3, -5);
189
+ scene.add(pointLight2);
190
+
191
+ // Camera
192
+ const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
193
+ camera.position.set(0, 2, 5);
194
+ console.log("Camera set to:", camera.position.x, camera.position.y, camera.position.z);
195
+
196
+ // Renderer
197
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
198
+ renderer.setSize(window.innerWidth, window.innerHeight);
199
+ renderer.setPixelRatio(window.devicePixelRatio);
200
+ document.body.appendChild(renderer.domElement);
201
+
202
+ // Orbit controls for non-VR
203
+ const controls = new OrbitControls(camera, renderer.domElement);
204
+ controls.enableDamping = true;
205
+ controls.dampingFactor = 0.05;
206
+ log(`Camera before: (${camera.position.x}, ${camera.position.y}, ${camera.position.z})`);
207
+ log("OrbitControls initialized - drag to rotate, scroll to zoom");
208
+
209
+ // Debug: log when controls are used
210
+ let lastLogTime = 0;
211
+ controls.addEventListener('change', () => {
212
+ const now = Date.now();
213
+ if (now - lastLogTime > 1000) {
214
+ const pos = camera.position;
215
+ log(`Camera: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`);
216
+ lastLogTime = now;
217
+ }
218
+ });
219
+
220
+ // Handle resize
221
+ window.addEventListener("resize", () => {
222
+ camera.aspect = window.innerWidth / window.innerHeight;
223
+ camera.updateProjectionMatrix();
224
+ renderer.setSize(window.innerWidth, window.innerHeight);
225
+ });
226
+
227
+ // Animation
228
+ let time = 0;
229
+ function animate() {
230
+ time += 0.016;
231
+
232
+ // Rotate cube
233
+ cube.rotation.x += 0.01;
234
+ cube.rotation.y += 0.015;
235
+
236
+ // Animate spheres in orbit
237
+ spheres.forEach((sphere, i) => {
238
+ const data = sphere.userData;
239
+ data.angle += 0.01 * data.speed;
240
+ sphere.position.x = Math.cos(data.angle) * data.radius;
241
+ sphere.position.z = Math.sin(data.angle) * data.radius;
242
+ sphere.position.y = 1 + Math.sin(time * data.speed + i) * 0.5 + data.yOffset;
243
+ });
244
+
245
+ // Rotate torus
246
+ torus.rotation.x += 0.005;
247
+ torus.rotation.y += 0.01;
248
+
249
+ // Slowly rotate stars
250
+ stars.rotation.y += 0.0002;
251
+
252
+ controls.update();
253
+ }
254
+
255
+ // Render loop
256
+ function render() {
257
+ requestAnimationFrame(render);
258
+ animate();
259
+ renderer.render(scene, camera);
260
+ }
261
+ render();
262
+
263
+ log("3D scene initialized");
264
+
265
+ // WebXR VR support
266
+ async function checkVR() {
267
+ if (!navigator.xr) {
268
+ log("WebXR not available");
269
+ vrButton.textContent = "WebXR Not Available";
270
+ return;
271
+ }
272
+
273
+ try {
274
+ const supported = await navigator.xr.isSessionSupported("immersive-vr");
275
+ if (supported) {
276
+ log("VR supported!");
277
+ vrButton.disabled = false;
278
+ vrButton.textContent = "Enter VR";
279
+ } else {
280
+ log("VR not supported (install WebXR Emulator)");
281
+ vrButton.textContent = "VR Not Supported";
282
+ }
283
+ } catch (e) {
284
+ log("VR check error: " + e.message);
285
+ }
286
+ }
287
+
288
+ vrButton.addEventListener("click", async () => {
289
+ if (!renderer.xr.isPresenting) {
290
+ try {
291
+ // Enable XR for VR session
292
+ renderer.xr.enabled = true;
293
+
294
+ const session = await navigator.xr.requestSession("immersive-vr", {
295
+ requiredFeatures: ["local-floor"]
296
+ });
297
+
298
+ renderer.xr.setReferenceSpaceType("local-floor");
299
+ await renderer.xr.setSession(session);
300
+
301
+ // Use XR animation loop
302
+ renderer.setAnimationLoop(() => {
303
+ animate();
304
+ renderer.render(scene, camera);
305
+ });
306
+
307
+ log("Entered VR!");
308
+ vrButton.textContent = "Exit VR";
309
+
310
+ session.addEventListener("end", () => {
311
+ log("Exited VR");
312
+ vrButton.textContent = "Enter VR";
313
+ renderer.xr.enabled = false;
314
+ renderer.setAnimationLoop(null);
315
+ render(); // Resume normal render loop
316
+ });
317
+
318
+ } catch (e) {
319
+ log("VR Error: " + e.message);
320
+ }
321
+ } else {
322
+ renderer.xr.getSession().end();
323
+ }
324
+ });
325
+
326
+ checkVR();
327
+ log("Ready - drag to rotate, scroll to zoom");
328
+ </script>
329
+ </body>
330
+ </html>
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebXR
4
+ module AR
5
+ # XRAnchor - An anchor in the real world
6
+ # Anchors are used to place virtual content at fixed positions in AR
7
+ class Anchor < JSWrapper
8
+ # @param js_anchor [JS::Object] The XRAnchor JavaScript object
9
+ def initialize(js_anchor)
10
+ super(js_anchor)
11
+ @deleted = false
12
+ end
13
+
14
+ # Get the anchor's coordinate space
15
+ # @return [Space]
16
+ def anchor_space
17
+ Space.new(js_prop(:anchorSpace))
18
+ end
19
+
20
+ # Delete the anchor
21
+ # After deletion, the anchor cannot be used
22
+ # @return [void]
23
+ def delete
24
+ js_call(:delete)
25
+ @deleted = true
26
+ end
27
+
28
+ # Check if the anchor has been deleted
29
+ # @return [Boolean]
30
+ def deleted?
31
+ @deleted
32
+ end
33
+
34
+ # Request persistent handle for this anchor
35
+ # Allows the anchor to be restored in future sessions
36
+ # @return [String] The persistent handle
37
+ def request_persistent_handle
38
+ promise = js_call(:requestPersistentHandle)
39
+ js_await(promise).to_s
40
+ end
41
+ end
42
+
43
+ # XRAnchorSet - A set of anchors
44
+ class AnchorSet < JSWrapper
45
+ include Enumerable
46
+
47
+ # @param js_anchor_set [JS::Object] The XRAnchorSet JavaScript object
48
+ def initialize(js_anchor_set)
49
+ super(js_anchor_set)
50
+ end
51
+
52
+ # Get the number of anchors
53
+ # @return [Integer]
54
+ def size
55
+ js_int(:size) || 0
56
+ end
57
+ alias length size
58
+
59
+ # Iterate over all anchors
60
+ # @yield [Anchor] Each anchor in the set
61
+ # @return [Enumerator, void]
62
+ def each(&block)
63
+ return enum_for(:each) unless block_given?
64
+
65
+ js_call(:forEach, ->(js_anchor) { yield Anchor.new(js_anchor) })
66
+ end
67
+
68
+ # Check if the set is empty
69
+ # @return [Boolean]
70
+ def empty?
71
+ size.zero?
72
+ end
73
+
74
+ # Convert to array
75
+ # @return [Array<Anchor>]
76
+ def to_a
77
+ anchors = []
78
+ each { |anchor| anchors << anchor }
79
+ anchors
80
+ end
81
+ end
82
+ end
83
+ end