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,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRSystem - Entry point for WebXR API
|
|
5
|
+
# Accessed via navigator.xr in JavaScript
|
|
6
|
+
class System < JSWrapper
|
|
7
|
+
@instance = nil
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
# Get the singleton XRSystem instance
|
|
11
|
+
# @return [System, nil] The XRSystem instance, or nil if WebXR is not available
|
|
12
|
+
def instance
|
|
13
|
+
@instance ||= begin
|
|
14
|
+
js_xr = JS.global[:navigator][:xr]
|
|
15
|
+
js_xr.nil? ? nil : new(js_xr)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check if WebXR is available in the current environment
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def available?
|
|
22
|
+
!instance.nil?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Reset the singleton instance (useful for testing)
|
|
26
|
+
# @api private
|
|
27
|
+
def reset_instance!
|
|
28
|
+
@instance = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param js_xr [JS::Object] The navigator.xr JavaScript object
|
|
33
|
+
def initialize(js_xr)
|
|
34
|
+
super(js_xr)
|
|
35
|
+
@device_change_callbacks = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if a session mode is supported
|
|
39
|
+
# @param mode [String] The session mode ("inline", "immersive-vr", "immersive-ar")
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def session_supported?(mode)
|
|
42
|
+
promise = js_call(:isSessionSupported, mode)
|
|
43
|
+
result = js_await(promise)
|
|
44
|
+
!!result
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Request a new XR session
|
|
48
|
+
# @param mode [String] The session mode
|
|
49
|
+
# @param required_features [Array<String>] Required features for the session
|
|
50
|
+
# @param optional_features [Array<String>] Optional features for the session
|
|
51
|
+
# @return [Session] The created XR session
|
|
52
|
+
# @raise [NotSupportedError] If the session mode is not supported
|
|
53
|
+
# @raise [NotAllowedError] If the user denies the session request
|
|
54
|
+
def request_session(mode, required_features: [], optional_features: [])
|
|
55
|
+
js_options = build_session_options(
|
|
56
|
+
required_features: required_features,
|
|
57
|
+
optional_features: optional_features
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
promise = js_call(:requestSession, mode, js_options)
|
|
61
|
+
js_session = js_await(promise)
|
|
62
|
+
Session.new(js_session)
|
|
63
|
+
rescue JS::Error => e
|
|
64
|
+
handle_js_error(e)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Register a callback for device change events
|
|
68
|
+
# @yield Called when XR devices are connected or disconnected
|
|
69
|
+
# @return [void]
|
|
70
|
+
def on_device_change(&block)
|
|
71
|
+
return unless block_given?
|
|
72
|
+
|
|
73
|
+
@device_change_callbacks << block
|
|
74
|
+
setup_device_change_listener if @device_change_callbacks.size == 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def build_session_options(required_features:, optional_features:)
|
|
80
|
+
js_options = JS.eval("({})")
|
|
81
|
+
|
|
82
|
+
unless required_features.empty?
|
|
83
|
+
js_required = to_js_array(required_features)
|
|
84
|
+
js_options[:requiredFeatures] = js_required
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
unless optional_features.empty?
|
|
88
|
+
js_optional = to_js_array(optional_features)
|
|
89
|
+
js_options[:optionalFeatures] = js_optional
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
js_options
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def setup_device_change_listener
|
|
96
|
+
callback = ->(event) { dispatch_device_change(event) }
|
|
97
|
+
js_callback = JS.eval("(function(event) { return $callback.call(event); })")
|
|
98
|
+
js_call(:addEventListener, "devicechange", js_callback)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def dispatch_device_change(event)
|
|
102
|
+
@device_change_callbacks.each { |cb| cb.call(event) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def handle_js_error(error)
|
|
106
|
+
message = error.message.to_s
|
|
107
|
+
|
|
108
|
+
case message
|
|
109
|
+
when /NotSupportedError/
|
|
110
|
+
raise NotSupportedError, message
|
|
111
|
+
when /SecurityError/
|
|
112
|
+
raise SecurityError, message
|
|
113
|
+
when /NotAllowedError/
|
|
114
|
+
raise NotAllowedError, message
|
|
115
|
+
when /InvalidStateError/
|
|
116
|
+
raise InvalidStateError, message
|
|
117
|
+
else
|
|
118
|
+
raise Error, message
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/webxr/errors.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# Base error class for all WebXR-related errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the requested feature is not supported by the browser or device
|
|
8
|
+
class NotSupportedError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when a security policy prevents the operation
|
|
11
|
+
class SecurityError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when an operation is performed in an invalid state
|
|
14
|
+
class InvalidStateError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when the user or system denies the operation
|
|
17
|
+
class NotAllowedError < Error; end
|
|
18
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRInputSourceEvent - Event for input source actions (select, squeeze)
|
|
5
|
+
class InputSourceEvent < SessionEvent
|
|
6
|
+
# Get the input source that triggered this event
|
|
7
|
+
# @return [InputSource]
|
|
8
|
+
def input_source
|
|
9
|
+
InputSource.new(js_prop(:inputSource))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get the frame at the time of the event
|
|
13
|
+
# @return [Frame]
|
|
14
|
+
def frame
|
|
15
|
+
Frame.new(js_prop(:frame))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Convenience method to get the handedness
|
|
19
|
+
# @return [String] "none", "left", or "right"
|
|
20
|
+
def handedness
|
|
21
|
+
input_source.handedness
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Check if this event is from the left controller
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def left?
|
|
27
|
+
input_source.left?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check if this event is from the right controller
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
def right?
|
|
33
|
+
input_source.right?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get the pose of the input source at the time of the event
|
|
37
|
+
# @param reference_space [ReferenceSpace] The reference space
|
|
38
|
+
# @return [Pose, nil]
|
|
39
|
+
def pose(reference_space)
|
|
40
|
+
frame.pose(input_source.target_ray_space, reference_space)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get the grip pose of the input source at the time of the event
|
|
44
|
+
# @param reference_space [ReferenceSpace] The reference space
|
|
45
|
+
# @return [Pose, nil]
|
|
46
|
+
def grip_pose(reference_space)
|
|
47
|
+
grip = input_source.grip_space
|
|
48
|
+
return nil if grip.nil?
|
|
49
|
+
|
|
50
|
+
frame.pose(grip, reference_space)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRReferenceSpaceEvent - Event fired when a reference space is reset
|
|
5
|
+
class ReferenceSpaceEvent < JSWrapper
|
|
6
|
+
# @param js_event [JS::Object] The JavaScript event object
|
|
7
|
+
def initialize(js_event)
|
|
8
|
+
super(js_event)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get the event type
|
|
12
|
+
# @return [String]
|
|
13
|
+
def type
|
|
14
|
+
js_string(:type) || ""
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Get the reference space that was reset
|
|
18
|
+
# @return [ReferenceSpace]
|
|
19
|
+
def reference_space
|
|
20
|
+
ReferenceSpace.new(js_prop(:referenceSpace))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get the transform representing the change in origin
|
|
24
|
+
# @return [RigidTransform, nil]
|
|
25
|
+
def transform
|
|
26
|
+
js_transform = js_prop(:transform)
|
|
27
|
+
return nil if js_transform.nil?
|
|
28
|
+
|
|
29
|
+
RigidTransform.new(js_transform)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get the event timestamp
|
|
33
|
+
# @return [Float]
|
|
34
|
+
def time_stamp
|
|
35
|
+
js_float(:timeStamp) || 0.0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if the event is trusted
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def trusted?
|
|
41
|
+
js_bool(:isTrusted)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRSessionEvent - Base event for session-related events
|
|
5
|
+
class SessionEvent < JSWrapper
|
|
6
|
+
# @param js_event [JS::Object] The JavaScript event object
|
|
7
|
+
def initialize(js_event)
|
|
8
|
+
super(js_event)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get the event type
|
|
12
|
+
# @return [String]
|
|
13
|
+
def type
|
|
14
|
+
js_string(:type) || ""
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Get the session associated with this event
|
|
18
|
+
# @return [Session]
|
|
19
|
+
def session
|
|
20
|
+
Session.new(js_prop(:session))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get the event timestamp
|
|
24
|
+
# @return [Float]
|
|
25
|
+
def time_stamp
|
|
26
|
+
js_float(:timeStamp) || 0.0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if the event is trusted
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def trusted?
|
|
32
|
+
js_bool(:isTrusted)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# XRInputSourcesChangeEvent - Event fired when input sources change
|
|
37
|
+
class InputSourcesChangeEvent < SessionEvent
|
|
38
|
+
# Get the added input sources
|
|
39
|
+
# @return [Array<InputSource>]
|
|
40
|
+
def added
|
|
41
|
+
js_added = js_prop(:added)
|
|
42
|
+
return [] if js_added.nil?
|
|
43
|
+
|
|
44
|
+
js_array_to_a(js_added).map { |s| InputSource.new(s) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get the removed input sources
|
|
48
|
+
# @return [Array<InputSource>]
|
|
49
|
+
def removed
|
|
50
|
+
js_removed = js_prop(:removed)
|
|
51
|
+
return [] if js_removed.nil?
|
|
52
|
+
|
|
53
|
+
js_array_to_a(js_removed).map { |s| InputSource.new(s) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRPose - Represents the position and orientation of a tracked object
|
|
5
|
+
class Pose < JSWrapper
|
|
6
|
+
# @param js_pose [JS::Object] The XRPose JavaScript object
|
|
7
|
+
def initialize(js_pose)
|
|
8
|
+
super(js_pose)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get the rigid transform for this pose
|
|
12
|
+
# @return [RigidTransform]
|
|
13
|
+
def transform
|
|
14
|
+
RigidTransform.new(js_prop(:transform))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Check if the position is emulated (not directly tracked)
|
|
18
|
+
# @return [Boolean]
|
|
19
|
+
def emulated_position?
|
|
20
|
+
js_bool(:emulatedPosition)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get the linear velocity in m/s
|
|
24
|
+
# @return [Hash, nil] Hash with :x, :y, :z keys, or nil if not available
|
|
25
|
+
def linear_velocity
|
|
26
|
+
v = js_prop(:linearVelocity)
|
|
27
|
+
return nil if v.nil?
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
x: v[:x].to_f,
|
|
31
|
+
y: v[:y].to_f,
|
|
32
|
+
z: v[:z].to_f
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get the angular velocity in rad/s
|
|
37
|
+
# @return [Hash, nil] Hash with :x, :y, :z keys, or nil if not available
|
|
38
|
+
def angular_velocity
|
|
39
|
+
v = js_prop(:angularVelocity)
|
|
40
|
+
return nil if v.nil?
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
x: v[:x].to_f,
|
|
44
|
+
y: v[:y].to_f,
|
|
45
|
+
z: v[:z].to_f
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRRigidTransform - Represents a position and orientation in 3D space
|
|
5
|
+
# Immutable transform consisting of a position and quaternion orientation
|
|
6
|
+
class RigidTransform < JSWrapper
|
|
7
|
+
# Create a new rigid transform
|
|
8
|
+
# @param js_transform [JS::Object, nil] Existing JS transform to wrap
|
|
9
|
+
# @param position [Hash, nil] Position hash with :x, :y, :z, :w keys
|
|
10
|
+
# @param orientation [Hash, nil] Orientation quaternion hash with :x, :y, :z, :w keys
|
|
11
|
+
def initialize(js_transform = nil, position: nil, orientation: nil)
|
|
12
|
+
if js_transform
|
|
13
|
+
super(js_transform)
|
|
14
|
+
else
|
|
15
|
+
js_position = position ? create_dom_point(position) : nil
|
|
16
|
+
js_orientation = orientation ? create_dom_point(orientation) : nil
|
|
17
|
+
js_obj = JS.global[:XRRigidTransform].new(js_position, js_orientation)
|
|
18
|
+
super(js_obj)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get the position as a hash
|
|
23
|
+
# @return [Hash] Hash with :x, :y, :z, :w keys
|
|
24
|
+
def position
|
|
25
|
+
p = js_prop(:position)
|
|
26
|
+
{
|
|
27
|
+
x: p[:x].to_f,
|
|
28
|
+
y: p[:y].to_f,
|
|
29
|
+
z: p[:z].to_f,
|
|
30
|
+
w: p[:w].to_f
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get the orientation quaternion as a hash
|
|
35
|
+
# @return [Hash] Hash with :x, :y, :z, :w keys
|
|
36
|
+
def orientation
|
|
37
|
+
o = js_prop(:orientation)
|
|
38
|
+
{
|
|
39
|
+
x: o[:x].to_f,
|
|
40
|
+
y: o[:y].to_f,
|
|
41
|
+
z: o[:z].to_f,
|
|
42
|
+
w: o[:w].to_f
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get the 4x4 transformation matrix
|
|
47
|
+
# @return [Array<Float>] 16-element array in column-major order
|
|
48
|
+
def matrix
|
|
49
|
+
js_matrix = js_prop(:matrix)
|
|
50
|
+
js_array_to_a(js_matrix).map(&:to_f)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get the inverse transform
|
|
54
|
+
# @return [RigidTransform]
|
|
55
|
+
def inverse
|
|
56
|
+
RigidTransform.new(js_prop(:inverse))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the position as a 3-element array [x, y, z]
|
|
60
|
+
# @return [Array<Float>]
|
|
61
|
+
def position_array
|
|
62
|
+
p = position
|
|
63
|
+
[p[:x], p[:y], p[:z]]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get the orientation as a 4-element array [x, y, z, w]
|
|
67
|
+
# @return [Array<Float>]
|
|
68
|
+
def orientation_array
|
|
69
|
+
o = orientation
|
|
70
|
+
[o[:x], o[:y], o[:z], o[:w]]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRView - Represents a single view (eye) for rendering
|
|
5
|
+
class View < JSWrapper
|
|
6
|
+
# @param js_view [JS::Object] The XRView JavaScript object
|
|
7
|
+
def initialize(js_view)
|
|
8
|
+
super(js_view)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get the eye this view is for
|
|
12
|
+
# @return [String] "left", "right", or "none"
|
|
13
|
+
def eye
|
|
14
|
+
js_string(:eye) || Eye::NONE
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Get the 4x4 projection matrix
|
|
18
|
+
# @return [Array<Float>] 16-element array in column-major order
|
|
19
|
+
def projection_matrix
|
|
20
|
+
js_matrix = js_prop(:projectionMatrix)
|
|
21
|
+
js_array_to_a(js_matrix).map(&:to_f)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get the view transform
|
|
25
|
+
# @return [RigidTransform]
|
|
26
|
+
def transform
|
|
27
|
+
RigidTransform.new(js_prop(:transform))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get the recommended viewport scale
|
|
31
|
+
# @return [Float, nil]
|
|
32
|
+
def recommended_viewport_scale
|
|
33
|
+
js_float(:recommendedViewportScale)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Request a specific viewport scale
|
|
37
|
+
# @param scale [Float] Scale factor (0.0 to 1.0)
|
|
38
|
+
# @return [void]
|
|
39
|
+
def request_viewport_scale(scale)
|
|
40
|
+
js_call(:requestViewportScale, scale)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if this is the left eye
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def left?
|
|
46
|
+
eye == Eye::LEFT
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if this is the right eye
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def right?
|
|
52
|
+
eye == Eye::RIGHT
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if this is a mono view (no stereo)
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def mono?
|
|
58
|
+
eye == Eye::NONE
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get the view matrix (inverse of transform)
|
|
62
|
+
# Useful for rendering calculations
|
|
63
|
+
# @return [Array<Float>] 16-element array in column-major order
|
|
64
|
+
def view_matrix
|
|
65
|
+
transform.inverse.matrix
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRViewerPose - The pose of the user's head/viewer
|
|
5
|
+
# Contains one or more views (eyes) for rendering
|
|
6
|
+
class ViewerPose < Pose
|
|
7
|
+
# @param js_viewer_pose [JS::Object] The XRViewerPose JavaScript object
|
|
8
|
+
def initialize(js_viewer_pose)
|
|
9
|
+
super(js_viewer_pose)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get all views (typically one per eye)
|
|
13
|
+
# @return [Array<View>]
|
|
14
|
+
def views
|
|
15
|
+
js_views = js_prop(:views)
|
|
16
|
+
return [] if js_views.nil?
|
|
17
|
+
|
|
18
|
+
js_array_to_a(js_views).map { |v| View.new(v) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get the left eye view
|
|
22
|
+
# @return [View, nil]
|
|
23
|
+
def left_view
|
|
24
|
+
views.find { |v| v.eye == Eye::LEFT }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Get the right eye view
|
|
28
|
+
# @return [View, nil]
|
|
29
|
+
def right_view
|
|
30
|
+
views.find { |v| v.eye == Eye::RIGHT }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Iterate over each view
|
|
34
|
+
# @yield [View] Each view
|
|
35
|
+
# @return [void]
|
|
36
|
+
def each_view(&block)
|
|
37
|
+
views.each(&block)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
# XRViewport - Defines a rectangular region for rendering
|
|
5
|
+
class Viewport < JSWrapper
|
|
6
|
+
# @param js_viewport [JS::Object] The XRViewport JavaScript object
|
|
7
|
+
def initialize(js_viewport)
|
|
8
|
+
super(js_viewport)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get the X offset in pixels
|
|
12
|
+
# @return [Integer]
|
|
13
|
+
def x
|
|
14
|
+
js_int(:x) || 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Get the Y offset in pixels
|
|
18
|
+
# @return [Integer]
|
|
19
|
+
def y
|
|
20
|
+
js_int(:y) || 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get the width in pixels
|
|
24
|
+
# @return [Integer]
|
|
25
|
+
def width
|
|
26
|
+
js_int(:width) || 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get the height in pixels
|
|
30
|
+
# @return [Integer]
|
|
31
|
+
def height
|
|
32
|
+
js_int(:height) || 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Convert to a hash
|
|
36
|
+
# @return [Hash] Hash with :x, :y, :width, :height keys
|
|
37
|
+
def to_h
|
|
38
|
+
{ x: x, y: y, width: width, height: height }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Convert to an array [x, y, width, height]
|
|
42
|
+
# @return [Array<Integer>]
|
|
43
|
+
def to_a
|
|
44
|
+
[x, y, width, height]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Calculate the aspect ratio
|
|
48
|
+
# @return [Float]
|
|
49
|
+
def aspect_ratio
|
|
50
|
+
return 0.0 if height.zero?
|
|
51
|
+
|
|
52
|
+
width.to_f / height.to_f
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|