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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/README.md +138 -0
- data/Rakefile +21 -0
- data/examples/ar_demo.html +238 -0
- data/examples/ar_hit_test.rb +157 -0
- data/examples/basic_vr.rb +110 -0
- data/examples/controller_input.rb +91 -0
- data/examples/hand_tracking.rb +124 -0
- data/examples/hello_webxr.html +288 -0
- data/examples/inline_demo.html +261 -0
- data/examples/server.rb +34 -0
- data/examples/vr_scene_demo.html +330 -0
- data/lib/webxr/ar/anchor.rb +83 -0
- data/lib/webxr/ar/hit_test_result.rb +54 -0
- data/lib/webxr/ar/hit_test_source.rb +34 -0
- data/lib/webxr/ar/ray.rb +90 -0
- data/lib/webxr/constants.rb +61 -0
- data/lib/webxr/core/frame.rb +155 -0
- data/lib/webxr/core/render_state.rb +47 -0
- data/lib/webxr/core/session.rb +212 -0
- data/lib/webxr/core/system.rb +122 -0
- data/lib/webxr/errors.rb +18 -0
- data/lib/webxr/events/input_source_event.rb +53 -0
- data/lib/webxr/events/reference_space_event.rb +44 -0
- data/lib/webxr/events/session_event.rb +56 -0
- data/lib/webxr/geometry/pose.rb +49 -0
- data/lib/webxr/geometry/rigid_transform.rb +73 -0
- data/lib/webxr/geometry/view.rb +68 -0
- data/lib/webxr/geometry/viewer_pose.rb +40 -0
- data/lib/webxr/geometry/viewport.rb +55 -0
- data/lib/webxr/hand/hand.rb +197 -0
- data/lib/webxr/hand/joint_pose.rb +33 -0
- data/lib/webxr/hand/joint_space.rb +74 -0
- data/lib/webxr/helpers/input_helper.rb +142 -0
- data/lib/webxr/helpers/rendering_helper.rb +94 -0
- data/lib/webxr/helpers/session_manager.rb +105 -0
- data/lib/webxr/input/gamepad.rb +115 -0
- data/lib/webxr/input/gamepad_button.rb +36 -0
- data/lib/webxr/input/input_source.rb +101 -0
- data/lib/webxr/input/input_source_array.rb +86 -0
- data/lib/webxr/js_wrapper.rb +116 -0
- data/lib/webxr/layers/layer.rb +28 -0
- data/lib/webxr/layers/webgl_binding.rb +69 -0
- data/lib/webxr/layers/webgl_layer.rb +102 -0
- data/lib/webxr/layers/webgl_sub_image.rb +59 -0
- data/lib/webxr/spaces/bounded_reference_space.rb +43 -0
- data/lib/webxr/spaces/reference_space.rb +51 -0
- data/lib/webxr/spaces/space.rb +18 -0
- data/lib/webxr/version.rb +5 -0
- data/lib/webxr.rb +73 -0
- data/webxr.gemspec +33 -0
- metadata +111 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module AR
|
|
5
|
+
# XRHitTestResult - Result of a hit test
|
|
6
|
+
class HitTestResult < JSWrapper
|
|
7
|
+
# @param js_result [JS::Object] The XRHitTestResult JavaScript object
|
|
8
|
+
def initialize(js_result)
|
|
9
|
+
super(js_result)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get the pose of the hit point relative to a base space
|
|
13
|
+
# @param base_space [Space] The base reference space
|
|
14
|
+
# @return [Pose, nil]
|
|
15
|
+
def pose(base_space)
|
|
16
|
+
js_pose = js_call(:getPose, base_space.js)
|
|
17
|
+
return nil if js_pose.nil?
|
|
18
|
+
|
|
19
|
+
Pose.new(js_pose)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Create an anchor at this hit test result
|
|
23
|
+
# @return [Anchor]
|
|
24
|
+
def create_anchor
|
|
25
|
+
promise = js_call(:createAnchor)
|
|
26
|
+
js_anchor = js_await(promise)
|
|
27
|
+
Anchor.new(js_anchor)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# XRTransientInputHitTestResult - Hit test result for transient input
|
|
32
|
+
class TransientInputHitTestResult < JSWrapper
|
|
33
|
+
# @param js_result [JS::Object] The XRTransientInputHitTestResult JavaScript object
|
|
34
|
+
def initialize(js_result)
|
|
35
|
+
super(js_result)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get the input source that triggered the hit test
|
|
39
|
+
# @return [InputSource]
|
|
40
|
+
def input_source
|
|
41
|
+
InputSource.new(js_prop(:inputSource))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get all hit test results for this input
|
|
45
|
+
# @return [Array<HitTestResult>]
|
|
46
|
+
def results
|
|
47
|
+
js_results = js_prop(:results)
|
|
48
|
+
return [] if js_results.nil?
|
|
49
|
+
|
|
50
|
+
js_array_to_a(js_results).map { |r| HitTestResult.new(r) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module AR
|
|
5
|
+
# XRHitTestSource - Source for performing hit tests
|
|
6
|
+
class HitTestSource < JSWrapper
|
|
7
|
+
# @param js_hit_test_source [JS::Object] The XRHitTestSource JavaScript object
|
|
8
|
+
def initialize(js_hit_test_source)
|
|
9
|
+
super(js_hit_test_source)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Cancel the hit test source
|
|
13
|
+
# Once cancelled, the source cannot be used for further hit tests
|
|
14
|
+
# @return [void]
|
|
15
|
+
def cancel
|
|
16
|
+
js_call(:cancel)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# XRTransientInputHitTestSource - Hit test source for transient input
|
|
21
|
+
class TransientInputHitTestSource < JSWrapper
|
|
22
|
+
# @param js_source [JS::Object] The XRTransientInputHitTestSource JavaScript object
|
|
23
|
+
def initialize(js_source)
|
|
24
|
+
super(js_source)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Cancel the hit test source
|
|
28
|
+
# @return [void]
|
|
29
|
+
def cancel
|
|
30
|
+
js_call(:cancel)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/webxr/ar/ray.rb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module AR
|
|
5
|
+
# XRRay - Represents a ray in 3D space for hit testing
|
|
6
|
+
class Ray < JSWrapper
|
|
7
|
+
# Create a new ray
|
|
8
|
+
# @param js_ray [JS::Object, nil] Existing JS ray to wrap
|
|
9
|
+
# @param origin [Hash, nil] Origin point with :x, :y, :z, :w keys
|
|
10
|
+
# @param direction [Hash, nil] Direction vector with :x, :y, :z, :w keys
|
|
11
|
+
# @param transform [RigidTransform, nil] Transform to apply
|
|
12
|
+
def initialize(js_ray = nil, origin: nil, direction: nil, transform: nil)
|
|
13
|
+
if js_ray
|
|
14
|
+
super(js_ray)
|
|
15
|
+
else
|
|
16
|
+
js_origin = origin ? create_dom_point(origin) : nil
|
|
17
|
+
js_direction = direction ? create_dom_point(direction) : nil
|
|
18
|
+
|
|
19
|
+
if transform
|
|
20
|
+
js_obj = JS.global[:XRRay].new(transform.js)
|
|
21
|
+
elsif js_origin && js_direction
|
|
22
|
+
js_obj = JS.global[:XRRay].new(js_origin, js_direction)
|
|
23
|
+
elsif js_origin
|
|
24
|
+
js_obj = JS.global[:XRRay].new(js_origin)
|
|
25
|
+
else
|
|
26
|
+
js_obj = JS.global[:XRRay].new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
super(js_obj)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get the origin point of the ray
|
|
34
|
+
# @return [Hash] Hash with :x, :y, :z, :w keys
|
|
35
|
+
def origin
|
|
36
|
+
o = js_prop(:origin)
|
|
37
|
+
{
|
|
38
|
+
x: o[:x].to_f,
|
|
39
|
+
y: o[:y].to_f,
|
|
40
|
+
z: o[:z].to_f,
|
|
41
|
+
w: o[:w].to_f
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get the direction vector of the ray (normalized)
|
|
46
|
+
# @return [Hash] Hash with :x, :y, :z, :w keys
|
|
47
|
+
def direction
|
|
48
|
+
d = js_prop(:direction)
|
|
49
|
+
{
|
|
50
|
+
x: d[:x].to_f,
|
|
51
|
+
y: d[:y].to_f,
|
|
52
|
+
z: d[:z].to_f,
|
|
53
|
+
w: d[:w].to_f
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get the 4x4 transformation matrix
|
|
58
|
+
# @return [Array<Float>] 16-element array in column-major order
|
|
59
|
+
def matrix
|
|
60
|
+
js_matrix = js_prop(:matrix)
|
|
61
|
+
js_array_to_a(js_matrix).map(&:to_f)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get the origin as a 3-element array [x, y, z]
|
|
65
|
+
# @return [Array<Float>]
|
|
66
|
+
def origin_array
|
|
67
|
+
o = origin
|
|
68
|
+
[o[:x], o[:y], o[:z]]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get the direction as a 3-element array [x, y, z]
|
|
72
|
+
# @return [Array<Float>]
|
|
73
|
+
def direction_array
|
|
74
|
+
d = direction
|
|
75
|
+
[d[:x], d[:y], d[:z]]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def create_dom_point(hash)
|
|
81
|
+
JS.global[:DOMPointReadOnly].new(
|
|
82
|
+
hash[:x] || 0,
|
|
83
|
+
hash[:y] || 0,
|
|
84
|
+
hash[:z] || (hash.key?(:z) ? hash[:z] : -1), # Default direction is -Z
|
|
85
|
+
hash[:w] || 1
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XR session mode types
|
|
5
|
+
module SessionMode
|
|
6
|
+
INLINE = "inline"
|
|
7
|
+
IMMERSIVE_VR = "immersive-vr"
|
|
8
|
+
IMMERSIVE_AR = "immersive-ar"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Reference space types
|
|
12
|
+
module ReferenceSpaceType
|
|
13
|
+
VIEWER = "viewer"
|
|
14
|
+
LOCAL = "local"
|
|
15
|
+
LOCAL_FLOOR = "local-floor"
|
|
16
|
+
BOUNDED_FLOOR = "bounded-floor"
|
|
17
|
+
UNBOUNDED = "unbounded"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Session visibility states
|
|
21
|
+
module VisibilityState
|
|
22
|
+
VISIBLE = "visible"
|
|
23
|
+
VISIBLE_BLURRED = "visible-blurred"
|
|
24
|
+
HIDDEN = "hidden"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Controller handedness
|
|
28
|
+
module Handedness
|
|
29
|
+
NONE = "none"
|
|
30
|
+
LEFT = "left"
|
|
31
|
+
RIGHT = "right"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Input source target ray modes
|
|
35
|
+
module TargetRayMode
|
|
36
|
+
GAZE = "gaze"
|
|
37
|
+
TRACKED_POINTER = "tracked-pointer"
|
|
38
|
+
SCREEN = "screen"
|
|
39
|
+
TRANSIENT_POINTER = "transient-pointer"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Environment blend modes (for AR)
|
|
43
|
+
module EnvironmentBlendMode
|
|
44
|
+
OPAQUE = "opaque"
|
|
45
|
+
ALPHA_BLEND = "alpha-blend"
|
|
46
|
+
ADDITIVE = "additive"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Interaction modes
|
|
50
|
+
module InteractionMode
|
|
51
|
+
SCREEN_SPACE = "screen-space"
|
|
52
|
+
WORLD_SPACE = "world-space"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Eye types for XRView
|
|
56
|
+
module Eye
|
|
57
|
+
NONE = "none"
|
|
58
|
+
LEFT = "left"
|
|
59
|
+
RIGHT = "right"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRFrame - Represents a single frame of an XR session
|
|
5
|
+
# Contains pose and timing information for rendering
|
|
6
|
+
class Frame < JSWrapper
|
|
7
|
+
# @param js_frame [JS::Object] The XRFrame JavaScript object
|
|
8
|
+
def initialize(js_frame)
|
|
9
|
+
super(js_frame)
|
|
10
|
+
@active = true
|
|
11
|
+
@animation_frame = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Get the session this frame belongs to
|
|
15
|
+
# @return [Session]
|
|
16
|
+
def session
|
|
17
|
+
Session.new(js_prop(:session))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get the predicted display time
|
|
21
|
+
# @return [Float] Time in milliseconds
|
|
22
|
+
def predicted_display_time
|
|
23
|
+
js_float(:predictedDisplayTime) || 0.0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get the viewer pose relative to a reference space
|
|
27
|
+
# @param reference_space [ReferenceSpace] The reference space
|
|
28
|
+
# @return [ViewerPose, nil]
|
|
29
|
+
def viewer_pose(reference_space)
|
|
30
|
+
js_pose = js_call(:getViewerPose, reference_space.js)
|
|
31
|
+
return nil if js_pose.nil?
|
|
32
|
+
|
|
33
|
+
ViewerPose.new(js_pose)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get a pose relative to a base space
|
|
37
|
+
# @param space [Space] The space to get the pose for
|
|
38
|
+
# @param base_space [Space] The base reference space
|
|
39
|
+
# @return [Pose, nil]
|
|
40
|
+
def pose(space, base_space)
|
|
41
|
+
js_pose = js_call(:getPose, space.js, base_space.js)
|
|
42
|
+
return nil if js_pose.nil?
|
|
43
|
+
|
|
44
|
+
Pose.new(js_pose)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get a joint pose (for hand tracking)
|
|
48
|
+
# @param joint [Hand::JointSpace] The joint space
|
|
49
|
+
# @param base_space [Space] The base reference space
|
|
50
|
+
# @return [Hand::JointPose, nil]
|
|
51
|
+
def joint_pose(joint, base_space)
|
|
52
|
+
js_pose = js_call(:getJointPose, joint.js, base_space.js)
|
|
53
|
+
return nil if js_pose.nil?
|
|
54
|
+
|
|
55
|
+
Hand::JointPose.new(js_pose)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Fill joint poses for an entire hand
|
|
59
|
+
# @param hand [Hand::Hand] The hand object
|
|
60
|
+
# @param base_space [Space] The base reference space
|
|
61
|
+
# @param joint_spaces [Array<Hand::JointSpace>] Array of joint spaces
|
|
62
|
+
# @param float32_array [JS::Object] Float32Array to fill
|
|
63
|
+
# @return [Boolean] True if all poses were filled
|
|
64
|
+
def fill_joint_poses(hand, base_space, joint_spaces, float32_array)
|
|
65
|
+
js_call(:fillJointPoses, joint_spaces.map(&:js), base_space.js, float32_array)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Fill a transform array from poses
|
|
69
|
+
# @param poses [Array<Pose>] Array of poses
|
|
70
|
+
# @param float32_array [JS::Object] Float32Array to fill
|
|
71
|
+
# @return [Boolean] True if all transforms were filled
|
|
72
|
+
def fill_poses(poses, float32_array)
|
|
73
|
+
js_call(:fillPoses, poses.map(&:js), float32_array)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get hit test results
|
|
77
|
+
# @param hit_test_source [AR::HitTestSource] The hit test source
|
|
78
|
+
# @return [Array<AR::HitTestResult>]
|
|
79
|
+
def hit_test_results(hit_test_source)
|
|
80
|
+
js_results = js_call(:getHitTestResults, hit_test_source.js)
|
|
81
|
+
return [] if js_results.nil?
|
|
82
|
+
|
|
83
|
+
js_array_to_a(js_results).map { |r| AR::HitTestResult.new(r) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get hit test results for transient input
|
|
87
|
+
# @param hit_test_source [AR::TransientInputHitTestSource] The hit test source
|
|
88
|
+
# @return [Array<AR::TransientInputHitTestResult>]
|
|
89
|
+
def hit_test_results_for_transient_input(hit_test_source)
|
|
90
|
+
js_results = js_call(:getHitTestResultsForTransientInput, hit_test_source.js)
|
|
91
|
+
return [] if js_results.nil?
|
|
92
|
+
|
|
93
|
+
js_array_to_a(js_results).map { |r| AR::TransientInputHitTestResult.new(r) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Create an anchor at a pose
|
|
97
|
+
# @param pose [RigidTransform] The transform for the anchor
|
|
98
|
+
# @param space [Space] The space for the anchor
|
|
99
|
+
# @return [AR::Anchor]
|
|
100
|
+
def create_anchor(pose, space)
|
|
101
|
+
promise = js_call(:createAnchor, pose.js, space.js)
|
|
102
|
+
js_anchor = js_await(promise)
|
|
103
|
+
AR::Anchor.new(js_anchor)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get tracked anchors
|
|
107
|
+
# @return [Set<AR::Anchor>]
|
|
108
|
+
def tracked_anchors
|
|
109
|
+
js_anchors = js_prop(:trackedAnchors)
|
|
110
|
+
return Set.new if js_anchors.nil?
|
|
111
|
+
|
|
112
|
+
anchors = Set.new
|
|
113
|
+
js_anchors.call(:forEach, ->(js_anchor) { anchors << AR::Anchor.new(js_anchor) })
|
|
114
|
+
anchors
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get the light estimate
|
|
118
|
+
# @param light_probe [Lighting::Probe] The light probe
|
|
119
|
+
# @return [Lighting::Estimate, nil]
|
|
120
|
+
def light_estimate(light_probe)
|
|
121
|
+
js_estimate = js_call(:getLightEstimate, light_probe.js)
|
|
122
|
+
return nil if js_estimate.nil?
|
|
123
|
+
|
|
124
|
+
Lighting::Estimate.new(js_estimate)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get depth information
|
|
128
|
+
# @param view [View] The view to get depth for
|
|
129
|
+
# @return [Depth::Information, nil]
|
|
130
|
+
def depth_information(view)
|
|
131
|
+
js_info = js_call(:getDepthInformation, view.js)
|
|
132
|
+
return nil if js_info.nil?
|
|
133
|
+
|
|
134
|
+
Depth::Information.new(js_info)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check if this frame is still active
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
def active?
|
|
140
|
+
@active
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Check if this is an animation frame
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
def animation_frame?
|
|
146
|
+
@animation_frame
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Mark the frame as inactive (called after callback completes)
|
|
150
|
+
# @api private
|
|
151
|
+
def mark_inactive!
|
|
152
|
+
@active = false
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRRenderState - Contains render configuration for a session
|
|
5
|
+
class RenderState < JSWrapper
|
|
6
|
+
# @param js_render_state [JS::Object] The XRRenderState JavaScript object
|
|
7
|
+
def initialize(js_render_state)
|
|
8
|
+
super(js_render_state)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get the near depth plane distance
|
|
12
|
+
# @return [Float]
|
|
13
|
+
def depth_near
|
|
14
|
+
js_float(:depthNear) || 0.1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Get the far depth plane distance
|
|
18
|
+
# @return [Float]
|
|
19
|
+
def depth_far
|
|
20
|
+
js_float(:depthFar) || 1000.0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get the inline vertical field of view (for inline sessions)
|
|
24
|
+
# @return [Float, nil] Field of view in radians
|
|
25
|
+
def inline_vertical_field_of_view
|
|
26
|
+
js_float(:inlineVerticalFieldOfView)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get the base XR layer
|
|
30
|
+
# @return [WebGLLayer, nil]
|
|
31
|
+
def base_layer
|
|
32
|
+
js_layer = js_prop(:baseLayer)
|
|
33
|
+
return nil if js_layer.nil?
|
|
34
|
+
|
|
35
|
+
WebGLLayer.wrap(js_layer)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get all layers (for multi-layer rendering)
|
|
39
|
+
# @return [Array<Layer>]
|
|
40
|
+
def layers
|
|
41
|
+
js_layers = js_prop(:layers)
|
|
42
|
+
return [] if js_layers.nil?
|
|
43
|
+
|
|
44
|
+
js_array_to_a(js_layers).map { |l| Layer.wrap(l) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRSession - Represents an XR session
|
|
5
|
+
# Manages the rendering loop and input sources
|
|
6
|
+
class Session < JSWrapper
|
|
7
|
+
# @return [String] The session mode ("inline", "immersive-vr", "immersive-ar")
|
|
8
|
+
attr_reader :mode
|
|
9
|
+
|
|
10
|
+
# Session event types
|
|
11
|
+
EVENTS = %i[
|
|
12
|
+
end
|
|
13
|
+
select
|
|
14
|
+
selectstart
|
|
15
|
+
selectend
|
|
16
|
+
squeeze
|
|
17
|
+
squeezestart
|
|
18
|
+
squeezeend
|
|
19
|
+
inputsourceschange
|
|
20
|
+
visibilitychange
|
|
21
|
+
frameratechange
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# @param js_session [JS::Object] The XRSession JavaScript object
|
|
25
|
+
def initialize(js_session)
|
|
26
|
+
super(js_session)
|
|
27
|
+
@mode = js_string(:mode)
|
|
28
|
+
@render_state = nil
|
|
29
|
+
@callbacks = EVENTS.each_with_object({}) { |event, h| h[event] = [] }
|
|
30
|
+
@animation_frame_id = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get the current visibility state
|
|
34
|
+
# @return [String] "visible", "visible-blurred", or "hidden"
|
|
35
|
+
def visibility_state
|
|
36
|
+
js_string(:visibilityState)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get the current frame rate
|
|
40
|
+
# @return [Float, nil]
|
|
41
|
+
def frame_rate
|
|
42
|
+
js_float(:frameRate)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get the supported frame rates
|
|
46
|
+
# @return [Array<Float>, nil]
|
|
47
|
+
def supported_frame_rates
|
|
48
|
+
js_array = js_prop(:supportedFrameRates)
|
|
49
|
+
return nil if js_array.nil?
|
|
50
|
+
|
|
51
|
+
js_array_to_a(js_array).map(&:to_f)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get the current render state
|
|
55
|
+
# @return [RenderState]
|
|
56
|
+
def render_state
|
|
57
|
+
@render_state ||= RenderState.new(js_prop(:renderState))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get the current input sources
|
|
61
|
+
# @return [InputSourceArray]
|
|
62
|
+
def input_sources
|
|
63
|
+
InputSourceArray.new(js_prop(:inputSources))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get the enabled features for this session
|
|
67
|
+
# @return [Array<String>]
|
|
68
|
+
def enabled_features
|
|
69
|
+
js_array = js_prop(:enabledFeatures)
|
|
70
|
+
return [] if js_array.nil?
|
|
71
|
+
|
|
72
|
+
js_array_to_a(js_array).map(&:to_s)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if system keyboard is supported
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def system_keyboard_supported?
|
|
78
|
+
js_bool(:isSystemKeyboardSupported)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get the environment blend mode
|
|
82
|
+
# @return [String] "opaque", "alpha-blend", or "additive"
|
|
83
|
+
def environment_blend_mode
|
|
84
|
+
js_string(:environmentBlendMode)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get the interaction mode
|
|
88
|
+
# @return [String] "screen-space" or "world-space"
|
|
89
|
+
def interaction_mode
|
|
90
|
+
js_string(:interactionMode)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if the session has ended
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def ended?
|
|
96
|
+
js_bool(:ended)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Update the render state
|
|
100
|
+
# @param depth_near [Float, nil] Near depth plane
|
|
101
|
+
# @param depth_far [Float, nil] Far depth plane
|
|
102
|
+
# @param inline_vertical_field_of_view [Float, nil] Field of view for inline sessions
|
|
103
|
+
# @param base_layer [WebGLLayer, nil] The base WebGL layer
|
|
104
|
+
# @return [void]
|
|
105
|
+
def update_render_state(depth_near: nil, depth_far: nil, inline_vertical_field_of_view: nil, base_layer: nil)
|
|
106
|
+
js_options = JS.eval("({})")
|
|
107
|
+
|
|
108
|
+
js_options[:depthNear] = depth_near if depth_near
|
|
109
|
+
js_options[:depthFar] = depth_far if depth_far
|
|
110
|
+
js_options[:inlineVerticalFieldOfView] = inline_vertical_field_of_view if inline_vertical_field_of_view
|
|
111
|
+
js_options[:baseLayer] = base_layer.js if base_layer
|
|
112
|
+
|
|
113
|
+
js_call(:updateRenderState, js_options)
|
|
114
|
+
@render_state = nil # Clear cache
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Update the target frame rate
|
|
118
|
+
# @param rate [Float] The target frame rate in Hz
|
|
119
|
+
# @return [void]
|
|
120
|
+
def update_target_frame_rate(rate)
|
|
121
|
+
promise = js_call(:updateTargetFrameRate, rate)
|
|
122
|
+
js_await(promise)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Request a reference space
|
|
126
|
+
# @param type [String] The reference space type
|
|
127
|
+
# @return [ReferenceSpace, BoundedReferenceSpace]
|
|
128
|
+
def request_reference_space(type)
|
|
129
|
+
promise = js_call(:requestReferenceSpace, type)
|
|
130
|
+
js_space = js_await(promise)
|
|
131
|
+
|
|
132
|
+
case type
|
|
133
|
+
when ReferenceSpaceType::BOUNDED_FLOOR
|
|
134
|
+
BoundedReferenceSpace.new(js_space)
|
|
135
|
+
else
|
|
136
|
+
ReferenceSpace.new(js_space)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Request an animation frame
|
|
141
|
+
# @yield [Float, Frame] The callback receives time and frame
|
|
142
|
+
# @return [Integer] The animation frame handle
|
|
143
|
+
def request_animation_frame(&block)
|
|
144
|
+
return nil unless block_given?
|
|
145
|
+
|
|
146
|
+
js_callback = create_animation_frame_callback(block)
|
|
147
|
+
@animation_frame_id = js_call(:requestAnimationFrame, js_callback)
|
|
148
|
+
@animation_frame_id.to_i
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Cancel a pending animation frame
|
|
152
|
+
# @param handle [Integer, nil] The animation frame handle (uses last handle if nil)
|
|
153
|
+
# @return [void]
|
|
154
|
+
def cancel_animation_frame(handle = nil)
|
|
155
|
+
handle ||= @animation_frame_id
|
|
156
|
+
js_call(:cancelAnimationFrame, handle) if handle
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# End the XR session
|
|
160
|
+
# @return [void]
|
|
161
|
+
def end_session
|
|
162
|
+
promise = js_call(:end)
|
|
163
|
+
js_await(promise)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Register an event handler
|
|
167
|
+
# @param event [Symbol, String] The event type
|
|
168
|
+
# @yield [Event] The event callback
|
|
169
|
+
# @return [void]
|
|
170
|
+
def on(event, &block)
|
|
171
|
+
event_sym = event.to_sym
|
|
172
|
+
return unless @callbacks.key?(event_sym) && block_given?
|
|
173
|
+
|
|
174
|
+
@callbacks[event_sym] << block
|
|
175
|
+
setup_event_listener(event_sym) if @callbacks[event_sym].size == 1
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def create_animation_frame_callback(block)
|
|
181
|
+
# Create a JS function that will invoke the Ruby block
|
|
182
|
+
JS.eval("(function(time, frame) {
|
|
183
|
+
return $callback.call(time, frame);
|
|
184
|
+
})")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def setup_event_listener(event_sym)
|
|
188
|
+
js_event = event_sym.to_s
|
|
189
|
+
|
|
190
|
+
callback = ->(js_event_obj) { dispatch_event(event_sym, js_event_obj) }
|
|
191
|
+
js_callback = JS.eval("(function(event) { return $callback.call(event); })")
|
|
192
|
+
|
|
193
|
+
js_call(:addEventListener, js_event, js_callback)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def dispatch_event(event_sym, js_event_obj)
|
|
197
|
+
event = wrap_event(event_sym, js_event_obj)
|
|
198
|
+
@callbacks[event_sym].each { |cb| cb.call(event) }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def wrap_event(event_sym, js_event_obj)
|
|
202
|
+
case event_sym
|
|
203
|
+
when :select, :selectstart, :selectend, :squeeze, :squeezestart, :squeezeend
|
|
204
|
+
InputSourceEvent.new(js_event_obj)
|
|
205
|
+
when :inputsourceschange
|
|
206
|
+
InputSourcesChangeEvent.new(js_event_obj)
|
|
207
|
+
else
|
|
208
|
+
SessionEvent.new(js_event_obj)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|