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,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebXR
4
+ module Hand
5
+ # Joint names as defined in WebXR Hand Input specification
6
+ JOINTS = %w[
7
+ wrist
8
+ thumb-metacarpal
9
+ thumb-phalanx-proximal
10
+ thumb-phalanx-distal
11
+ thumb-tip
12
+ index-finger-metacarpal
13
+ index-finger-phalanx-proximal
14
+ index-finger-phalanx-intermediate
15
+ index-finger-phalanx-distal
16
+ index-finger-tip
17
+ middle-finger-metacarpal
18
+ middle-finger-phalanx-proximal
19
+ middle-finger-phalanx-intermediate
20
+ middle-finger-phalanx-distal
21
+ middle-finger-tip
22
+ ring-finger-metacarpal
23
+ ring-finger-phalanx-proximal
24
+ ring-finger-phalanx-intermediate
25
+ ring-finger-phalanx-distal
26
+ ring-finger-tip
27
+ pinky-finger-metacarpal
28
+ pinky-finger-phalanx-proximal
29
+ pinky-finger-phalanx-intermediate
30
+ pinky-finger-phalanx-distal
31
+ pinky-finger-tip
32
+ ].freeze
33
+
34
+ # Number of joints in a hand
35
+ JOINT_COUNT = 25
36
+
37
+ # XRHand - Represents a tracked hand with 25 joints
38
+ class Hand < JSWrapper
39
+ include Enumerable
40
+
41
+ # @param js_hand [JS::Object] The XRHand JavaScript object
42
+ def initialize(js_hand)
43
+ super(js_hand)
44
+ end
45
+
46
+ # Get the number of joints
47
+ # @return [Integer] Always 25 for a complete hand
48
+ def size
49
+ js_int(:size) || JOINT_COUNT
50
+ end
51
+ alias length size
52
+
53
+ # Get a joint space by name or index
54
+ # @param key [String, Integer] Joint name or index
55
+ # @return [JointSpace, nil]
56
+ def [](key)
57
+ js_joint = case key
58
+ when Integer
59
+ joint_name = JOINTS[key]
60
+ return nil unless joint_name
61
+
62
+ js_call(:get, joint_name)
63
+ when String, Symbol
64
+ js_call(:get, key.to_s)
65
+ end
66
+
67
+ return nil if js_joint.nil?
68
+
69
+ JointSpace.new(js_joint)
70
+ end
71
+
72
+ # Get a joint space by name
73
+ # @param name [String] The joint name
74
+ # @return [JointSpace, nil]
75
+ def get(name)
76
+ js_joint = js_call(:get, name)
77
+ return nil if js_joint.nil?
78
+
79
+ JointSpace.new(js_joint)
80
+ end
81
+
82
+ # Iterate over all joint spaces
83
+ # @yield [String, JointSpace] Joint name and joint space
84
+ # @return [Enumerator, void]
85
+ def each(&block)
86
+ return enum_for(:each) unless block_given?
87
+
88
+ JOINTS.each do |name|
89
+ joint = get(name)
90
+ yield name, joint if joint
91
+ end
92
+ end
93
+
94
+ # Get all joint spaces as an array
95
+ # @return [Array<JointSpace>]
96
+ def to_a
97
+ JOINTS.map { |name| get(name) }.compact
98
+ end
99
+
100
+ # Get the wrist joint
101
+ # @return [JointSpace, nil]
102
+ def wrist
103
+ get("wrist")
104
+ end
105
+
106
+ # Get thumb joints
107
+ # @return [Array<JointSpace>]
108
+ def thumb
109
+ %w[thumb-metacarpal thumb-phalanx-proximal thumb-phalanx-distal thumb-tip].map { |n| get(n) }.compact
110
+ end
111
+
112
+ # Get the thumb tip
113
+ # @return [JointSpace, nil]
114
+ def thumb_tip
115
+ get("thumb-tip")
116
+ end
117
+
118
+ # Get index finger joints
119
+ # @return [Array<JointSpace>]
120
+ def index_finger
121
+ %w[
122
+ index-finger-metacarpal
123
+ index-finger-phalanx-proximal
124
+ index-finger-phalanx-intermediate
125
+ index-finger-phalanx-distal
126
+ index-finger-tip
127
+ ].map { |n| get(n) }.compact
128
+ end
129
+
130
+ # Get the index finger tip
131
+ # @return [JointSpace, nil]
132
+ def index_finger_tip
133
+ get("index-finger-tip")
134
+ end
135
+
136
+ # Get middle finger joints
137
+ # @return [Array<JointSpace>]
138
+ def middle_finger
139
+ %w[
140
+ middle-finger-metacarpal
141
+ middle-finger-phalanx-proximal
142
+ middle-finger-phalanx-intermediate
143
+ middle-finger-phalanx-distal
144
+ middle-finger-tip
145
+ ].map { |n| get(n) }.compact
146
+ end
147
+
148
+ # Get the middle finger tip
149
+ # @return [JointSpace, nil]
150
+ def middle_finger_tip
151
+ get("middle-finger-tip")
152
+ end
153
+
154
+ # Get ring finger joints
155
+ # @return [Array<JointSpace>]
156
+ def ring_finger
157
+ %w[
158
+ ring-finger-metacarpal
159
+ ring-finger-phalanx-proximal
160
+ ring-finger-phalanx-intermediate
161
+ ring-finger-phalanx-distal
162
+ ring-finger-tip
163
+ ].map { |n| get(n) }.compact
164
+ end
165
+
166
+ # Get the ring finger tip
167
+ # @return [JointSpace, nil]
168
+ def ring_finger_tip
169
+ get("ring-finger-tip")
170
+ end
171
+
172
+ # Get pinky finger joints
173
+ # @return [Array<JointSpace>]
174
+ def pinky_finger
175
+ %w[
176
+ pinky-finger-metacarpal
177
+ pinky-finger-phalanx-proximal
178
+ pinky-finger-phalanx-intermediate
179
+ pinky-finger-phalanx-distal
180
+ pinky-finger-tip
181
+ ].map { |n| get(n) }.compact
182
+ end
183
+
184
+ # Get the pinky finger tip
185
+ # @return [JointSpace, nil]
186
+ def pinky_finger_tip
187
+ get("pinky-finger-tip")
188
+ end
189
+
190
+ # Get all finger tips
191
+ # @return [Array<JointSpace>]
192
+ def finger_tips
193
+ [thumb_tip, index_finger_tip, middle_finger_tip, ring_finger_tip, pinky_finger_tip].compact
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebXR
4
+ module Hand
5
+ # XRJointPose - Represents the pose of a hand joint
6
+ # Includes radius information for collision detection
7
+ class JointPose < Pose
8
+ # @param js_joint_pose [JS::Object] The XRJointPose JavaScript object
9
+ def initialize(js_joint_pose)
10
+ super(js_joint_pose)
11
+ end
12
+
13
+ # Get the radius of the joint in meters
14
+ # This can be used for collision detection or visualization
15
+ # @return [Float]
16
+ def radius
17
+ js_float(:radius) || 0.0
18
+ end
19
+
20
+ # Get the diameter of the joint in meters
21
+ # @return [Float]
22
+ def diameter
23
+ radius * 2
24
+ end
25
+
26
+ # Check if the joint pose is valid (has meaningful data)
27
+ # @return [Boolean]
28
+ def valid?
29
+ radius > 0
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebXR
4
+ module Hand
5
+ # XRJointSpace - Represents a joint's coordinate space
6
+ # Each joint has its own space that can be used for pose calculations
7
+ class JointSpace < Space
8
+ # @param js_joint_space [JS::Object] The XRJointSpace JavaScript object
9
+ def initialize(js_joint_space)
10
+ super(js_joint_space)
11
+ end
12
+
13
+ # Get the joint name
14
+ # @return [String]
15
+ def joint_name
16
+ js_string(:jointName) || ""
17
+ end
18
+
19
+ # Get the space type
20
+ # @return [String]
21
+ def type
22
+ "joint-space"
23
+ end
24
+
25
+ # Check if this is a wrist joint
26
+ # @return [Boolean]
27
+ def wrist?
28
+ joint_name == "wrist"
29
+ end
30
+
31
+ # Check if this is a thumb joint
32
+ # @return [Boolean]
33
+ def thumb?
34
+ joint_name.start_with?("thumb")
35
+ end
36
+
37
+ # Check if this is an index finger joint
38
+ # @return [Boolean]
39
+ def index_finger?
40
+ joint_name.start_with?("index-finger")
41
+ end
42
+
43
+ # Check if this is a middle finger joint
44
+ # @return [Boolean]
45
+ def middle_finger?
46
+ joint_name.start_with?("middle-finger")
47
+ end
48
+
49
+ # Check if this is a ring finger joint
50
+ # @return [Boolean]
51
+ def ring_finger?
52
+ joint_name.start_with?("ring-finger")
53
+ end
54
+
55
+ # Check if this is a pinky finger joint
56
+ # @return [Boolean]
57
+ def pinky_finger?
58
+ joint_name.start_with?("pinky-finger")
59
+ end
60
+
61
+ # Check if this is a finger tip
62
+ # @return [Boolean]
63
+ def tip?
64
+ joint_name.end_with?("-tip")
65
+ end
66
+
67
+ # Check if this is a metacarpal joint
68
+ # @return [Boolean]
69
+ def metacarpal?
70
+ joint_name.end_with?("-metacarpal")
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebXR
4
+ module Helpers
5
+ # InputHelper - Simplifies XR input handling
6
+ class InputHelper
7
+ # @return [Session] The XR session
8
+ attr_reader :session
9
+
10
+ # @return [ReferenceSpace] The reference space
11
+ attr_reader :reference_space
12
+
13
+ # @param session [Session] The XR session
14
+ # @param reference_space [ReferenceSpace] The reference space for pose calculations
15
+ def initialize(session, reference_space)
16
+ @session = session
17
+ @reference_space = reference_space
18
+ @select_callbacks = []
19
+ @squeeze_callbacks = []
20
+ end
21
+
22
+ # Register a callback for select events
23
+ # @yield [InputSource, InputSourceEvent, Pose] Called on select
24
+ # @return [void]
25
+ def on_select(&block)
26
+ return unless block_given?
27
+
28
+ @select_callbacks << block
29
+ setup_select_listener if @select_callbacks.size == 1
30
+ end
31
+
32
+ # Register a callback for squeeze events
33
+ # @yield [InputSource, InputSourceEvent, Pose] Called on squeeze
34
+ # @return [void]
35
+ def on_squeeze(&block)
36
+ return unless block_given?
37
+
38
+ @squeeze_callbacks << block
39
+ setup_squeeze_listener if @squeeze_callbacks.size == 1
40
+ end
41
+
42
+ # Iterate over each controller (tracked pointers)
43
+ # @yield [InputSource] Each controller input source
44
+ # @return [void]
45
+ def each_controller(&block)
46
+ @session.input_sources.each do |source|
47
+ next unless source.tracked_pointer?
48
+
49
+ block.call(source)
50
+ end
51
+ end
52
+
53
+ # Get the left controller
54
+ # @return [InputSource, nil]
55
+ def left_controller
56
+ @session.input_sources.left
57
+ end
58
+
59
+ # Get the right controller
60
+ # @return [InputSource, nil]
61
+ def right_controller
62
+ @session.input_sources.right
63
+ end
64
+
65
+ # Get the primary controller (first available)
66
+ # @return [InputSource, nil]
67
+ def primary_controller
68
+ left_controller || right_controller || @session.input_sources.first
69
+ end
70
+
71
+ # Get the pose of an input source
72
+ # @param source [InputSource] The input source
73
+ # @param frame [Frame] The current frame
74
+ # @return [Pose, nil]
75
+ def pose_for(source, frame)
76
+ frame.pose(source.target_ray_space, @reference_space)
77
+ end
78
+
79
+ # Get the grip pose of an input source
80
+ # @param source [InputSource] The input source
81
+ # @param frame [Frame] The current frame
82
+ # @return [Pose, nil]
83
+ def grip_pose_for(source, frame)
84
+ grip = source.grip_space
85
+ return nil if grip.nil?
86
+
87
+ frame.pose(grip, @reference_space)
88
+ end
89
+
90
+ # Get button state for a controller
91
+ # @param source [InputSource] The input source
92
+ # @return [Hash] Hash with button states
93
+ def button_states(source)
94
+ gamepad = source.gamepad
95
+ return {} if gamepad.nil?
96
+
97
+ {
98
+ trigger: {
99
+ pressed: gamepad.trigger&.pressed?,
100
+ touched: gamepad.trigger&.touched?,
101
+ value: gamepad.trigger&.value || 0.0
102
+ },
103
+ squeeze: {
104
+ pressed: gamepad.squeeze&.pressed?,
105
+ touched: gamepad.squeeze&.touched?,
106
+ value: gamepad.squeeze&.value || 0.0
107
+ },
108
+ thumbstick: {
109
+ x: gamepad.thumbstick_x,
110
+ y: gamepad.thumbstick_y,
111
+ pressed: gamepad.thumbstick&.pressed?,
112
+ touched: gamepad.thumbstick&.touched?
113
+ },
114
+ touchpad: {
115
+ x: gamepad.touchpad_x,
116
+ y: gamepad.touchpad_y,
117
+ pressed: gamepad.touchpad&.pressed?,
118
+ touched: gamepad.touchpad&.touched?
119
+ }
120
+ }
121
+ end
122
+
123
+ private
124
+
125
+ def setup_select_listener
126
+ @session.on(:select) do |event|
127
+ source = event.input_source
128
+ pose = event.pose(@reference_space)
129
+ @select_callbacks.each { |cb| cb.call(source, event, pose) }
130
+ end
131
+ end
132
+
133
+ def setup_squeeze_listener
134
+ @session.on(:squeeze) do |event|
135
+ source = event.input_source
136
+ pose = event.pose(@reference_space)
137
+ @squeeze_callbacks.each { |cb| cb.call(source, event, pose) }
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebXR
4
+ module Helpers
5
+ # RenderingHelper - Simplifies WebXR rendering setup and loop
6
+ class RenderingHelper
7
+ # @return [WebGLLayer, nil] The WebGL layer
8
+ attr_reader :layer
9
+
10
+ # @return [Session] The XR session
11
+ attr_reader :session
12
+
13
+ # @return [ReferenceSpace] The reference space
14
+ attr_reader :reference_space
15
+
16
+ # @param session [Session] The XR session
17
+ # @param gl_context [JS::Object] The WebGL rendering context
18
+ # @param reference_space [ReferenceSpace] The reference space
19
+ def initialize(session, gl_context, reference_space)
20
+ @session = session
21
+ @gl = gl_context
22
+ @reference_space = reference_space
23
+ @layer = nil
24
+ @running = false
25
+ end
26
+
27
+ # Setup the WebGL layer
28
+ # @param options [Hash] Layer options
29
+ # @return [WebGLLayer]
30
+ def setup_layer(**options)
31
+ @layer = WebGLLayer.new(@session, @gl, **options)
32
+ @session.update_render_state(base_layer: @layer)
33
+ @layer
34
+ end
35
+
36
+ # Run the rendering frame loop
37
+ # @yield [Float, Frame, View, Viewport] Called for each view each frame
38
+ # @return [void]
39
+ def run_frame_loop(&render_callback)
40
+ @running = true
41
+
42
+ frame_callback = proc do |time, js_frame|
43
+ next unless @running && !@session.ended?
44
+
45
+ # Request next frame
46
+ @session.request_animation_frame(&frame_callback)
47
+
48
+ # Wrap the frame
49
+ frame = Frame.new(js_frame)
50
+
51
+ # Get viewer pose
52
+ pose = frame.viewer_pose(@reference_space)
53
+ next unless pose
54
+
55
+ # Bind framebuffer
56
+ @gl.call(:bindFramebuffer, @gl[:FRAMEBUFFER], @layer.framebuffer)
57
+
58
+ # Render each view
59
+ pose.views.each do |view|
60
+ viewport = @layer.viewport(view)
61
+ @gl.call(:viewport, viewport.x, viewport.y, viewport.width, viewport.height)
62
+
63
+ render_callback.call(time.to_f, frame, view, viewport)
64
+ end
65
+ end
66
+
67
+ @session.request_animation_frame(&frame_callback)
68
+ end
69
+
70
+ # Stop the rendering loop
71
+ # @return [void]
72
+ def stop
73
+ @running = false
74
+ end
75
+
76
+ # Check if the rendering loop is running
77
+ # @return [Boolean]
78
+ def running?
79
+ @running
80
+ end
81
+
82
+ # Clear the framebuffer
83
+ # @param r [Float] Red (0.0 to 1.0)
84
+ # @param g [Float] Green (0.0 to 1.0)
85
+ # @param b [Float] Blue (0.0 to 1.0)
86
+ # @param a [Float] Alpha (0.0 to 1.0)
87
+ # @return [void]
88
+ def clear(r: 0.0, g: 0.0, b: 0.0, a: 1.0)
89
+ @gl.call(:clearColor, r, g, b, a)
90
+ @gl.call(:clear, @gl[:COLOR_BUFFER_BIT] | @gl[:DEPTH_BUFFER_BIT])
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebXR
4
+ module Helpers
5
+ # SessionManager - Simplifies XR session lifecycle management
6
+ class SessionManager
7
+ # @return [Session, nil] The current session
8
+ attr_reader :session
9
+
10
+ # @return [ReferenceSpace, nil] The current reference space
11
+ attr_reader :reference_space
12
+
13
+ def initialize
14
+ @session = nil
15
+ @reference_space = nil
16
+ @xr_system = System.instance
17
+ end
18
+
19
+ # Check if WebXR is available
20
+ # @return [Boolean]
21
+ def available?
22
+ !@xr_system.nil?
23
+ end
24
+
25
+ # Check if a session mode is supported
26
+ # @param mode [String] The session mode to check
27
+ # @return [Boolean]
28
+ def supports?(mode)
29
+ return false unless available?
30
+
31
+ @xr_system.session_supported?(mode)
32
+ end
33
+
34
+ # Start an XR session
35
+ # @param mode [String] The session mode ("inline", "immersive-vr", "immersive-ar")
36
+ # @param reference_space_type [String] The reference space type
37
+ # @param required_features [Array<String>] Required features
38
+ # @param optional_features [Array<String>] Optional features
39
+ # @yield [Session, ReferenceSpace] Optional block for auto-cleanup
40
+ # @return [Session]
41
+ def start_session(mode, reference_space_type: ReferenceSpaceType::LOCAL_FLOOR,
42
+ required_features: [], optional_features: [])
43
+ raise NotSupportedError, "WebXR not available" unless available?
44
+
45
+ @session = @xr_system.request_session(
46
+ mode,
47
+ required_features: required_features,
48
+ optional_features: optional_features
49
+ )
50
+
51
+ @reference_space = @session.request_reference_space(reference_space_type)
52
+
53
+ if block_given?
54
+ begin
55
+ yield @session, @reference_space
56
+ ensure
57
+ end_session
58
+ end
59
+ end
60
+
61
+ @session
62
+ end
63
+
64
+ # End the current session
65
+ # @return [void]
66
+ def end_session
67
+ @session&.end_session
68
+ @session = nil
69
+ @reference_space = nil
70
+ end
71
+
72
+ # Check if a session is currently active
73
+ # @return [Boolean]
74
+ def active?
75
+ @session && !@session.ended?
76
+ end
77
+
78
+ # Start an immersive VR session
79
+ # @param options [Hash] Session options
80
+ # @yield [Session, ReferenceSpace] Optional block
81
+ # @return [Session]
82
+ def start_vr(**options, &block)
83
+ start_session(SessionMode::IMMERSIVE_VR, **options, &block)
84
+ end
85
+
86
+ # Start an immersive AR session
87
+ # @param options [Hash] Session options
88
+ # @yield [Session, ReferenceSpace] Optional block
89
+ # @return [Session]
90
+ def start_ar(**options, &block)
91
+ options[:reference_space_type] ||= ReferenceSpaceType::LOCAL
92
+ start_session(SessionMode::IMMERSIVE_AR, **options, &block)
93
+ end
94
+
95
+ # Start an inline session
96
+ # @param options [Hash] Session options
97
+ # @yield [Session, ReferenceSpace] Optional block
98
+ # @return [Session]
99
+ def start_inline(**options, &block)
100
+ options[:reference_space_type] ||= ReferenceSpaceType::VIEWER
101
+ start_session(SessionMode::INLINE, **options, &block)
102
+ end
103
+ end
104
+ end
105
+ end