Skip to main content

script/dom/webxr/
fakexrdevice.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::rc::Rc;
7
8use dom_struct::dom_struct;
9use euclid::{Point2D, Point3D, Rect, RigidTransform3D, Rotation3D, Size2D, Transform3D, Vector3D};
10use js::context::JSContext;
11use js::realm::CurrentRealm;
12use profile_traits::generic_callback::GenericCallback as ProfileGenericCallback;
13use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
14use servo_base::generic_channel::GenericSender;
15use webxr_api::{
16    EntityType, Handedness, InputId, InputSource, MockDeviceMsg, MockInputInit, MockRegion,
17    MockViewInit, MockViewsInit, MockWorld, TargetRayMode, Triangle, Visibility,
18};
19
20use crate::conversions::Convert;
21use crate::dom::bindings::codegen::Bindings::DOMPointBinding::DOMPointInit;
22use crate::dom::bindings::codegen::Bindings::FakeXRDeviceBinding::{
23    FakeXRBoundsPoint, FakeXRDeviceMethods, FakeXRRegionType, FakeXRRigidTransformInit,
24    FakeXRViewInit, FakeXRWorldInit,
25};
26use crate::dom::bindings::codegen::Bindings::FakeXRInputControllerBinding::FakeXRInputSourceInit;
27use crate::dom::bindings::codegen::Bindings::XRInputSourceBinding::{
28    XRHandedness, XRTargetRayMode,
29};
30use crate::dom::bindings::codegen::Bindings::XRSessionBinding::XRVisibilityState;
31use crate::dom::bindings::codegen::Bindings::XRViewBinding::XREye;
32use crate::dom::bindings::error::{Error, Fallible};
33use crate::dom::bindings::refcounted::TrustedPromise;
34use crate::dom::bindings::reflector::DomGlobal;
35use crate::dom::bindings::root::DomRoot;
36use crate::dom::fakexrinputcontroller::{FakeXRInputController, init_to_mock_buttons};
37use crate::dom::globalscope::GlobalScope;
38use crate::dom::promise::Promise;
39
40#[dom_struct]
41pub(crate) struct FakeXRDevice {
42    reflector: Reflector,
43    #[no_trace]
44    sender: GenericSender<MockDeviceMsg>,
45    #[no_trace]
46    next_input_id: Cell<InputId>,
47}
48
49impl FakeXRDevice {
50    pub(crate) fn new_inherited(sender: GenericSender<MockDeviceMsg>) -> FakeXRDevice {
51        FakeXRDevice {
52            reflector: Reflector::new(),
53            sender,
54            next_input_id: Cell::new(InputId(0)),
55        }
56    }
57
58    pub(crate) fn new(
59        cx: &mut JSContext,
60        global: &GlobalScope,
61        sender: GenericSender<MockDeviceMsg>,
62    ) -> DomRoot<FakeXRDevice> {
63        reflect_dom_object_with_cx(Box::new(FakeXRDevice::new_inherited(sender)), global, cx)
64    }
65
66    pub(crate) fn disconnect(&self, sender: ProfileGenericCallback<()>) {
67        let _ = self.sender.send(MockDeviceMsg::Disconnect(sender));
68    }
69}
70
71pub(crate) fn view<Eye>(view: &FakeXRViewInit) -> Fallible<MockViewInit<Eye>> {
72    if view.projectionMatrix.len() != 16 || view.viewOffset.position.len() != 3 {
73        return Err(Error::Type(c"Incorrectly sized array".into()));
74    }
75
76    let mut proj = [0.; 16];
77    let v: Vec<_> = view.projectionMatrix.iter().map(|x| **x).collect();
78    proj.copy_from_slice(&v);
79    let projection = Transform3D::from_array(proj);
80
81    // spec defines offsets as origins, but mock API expects the inverse transform
82    let transform = get_origin(&view.viewOffset)?.inverse();
83
84    let size = Size2D::new(view.resolution.width, view.resolution.height);
85    let origin = match view.eye {
86        XREye::Right => Point2D::new(size.width, 0),
87        _ => Point2D::zero(),
88    };
89    let viewport = Rect::new(origin, size);
90
91    let fov = view.fieldOfView.as_ref().map(|fov| {
92        (
93            fov.leftDegrees.to_radians(),
94            fov.rightDegrees.to_radians(),
95            fov.upDegrees.to_radians(),
96            fov.downDegrees.to_radians(),
97        )
98    });
99
100    Ok(MockViewInit {
101        projection,
102        transform,
103        viewport,
104        fov,
105    })
106}
107
108pub(crate) fn get_views(views: &[FakeXRViewInit]) -> Fallible<MockViewsInit> {
109    match views.len() {
110        1 => Ok(MockViewsInit::Mono(view(&views[0])?)),
111        2 => {
112            let (left, right) = match (views[0].eye, views[1].eye) {
113                (XREye::Left, XREye::Right) => (&views[0], &views[1]),
114                (XREye::Right, XREye::Left) => (&views[1], &views[0]),
115                _ => return Err(Error::NotSupported(None)),
116            };
117            Ok(MockViewsInit::Stereo(view(left)?, view(right)?))
118        },
119        _ => Err(Error::NotSupported(None)),
120    }
121}
122
123pub(crate) fn get_origin<T, U>(
124    origin: &FakeXRRigidTransformInit,
125) -> Fallible<RigidTransform3D<f32, T, U>> {
126    if origin.position.len() != 3 || origin.orientation.len() != 4 {
127        return Err(Error::Type(c"Incorrectly sized array".into()));
128    }
129    let p = Vector3D::new(
130        *origin.position[0],
131        *origin.position[1],
132        *origin.position[2],
133    );
134    let o = Rotation3D::unit_quaternion(
135        *origin.orientation[0],
136        *origin.orientation[1],
137        *origin.orientation[2],
138        *origin.orientation[3],
139    );
140
141    Ok(RigidTransform3D::new(o, p))
142}
143
144pub(crate) fn get_point<T>(pt: &DOMPointInit) -> Point3D<f32, T> {
145    Point3D::new(pt.x / pt.w, pt.y / pt.w, pt.z / pt.w).cast()
146}
147
148pub(crate) fn get_world(world: &FakeXRWorldInit) -> Fallible<MockWorld> {
149    let regions = world
150        .hitTestRegions
151        .iter()
152        .map(|region| {
153            let ty = region.type_.convert();
154            let faces = region
155                .faces
156                .iter()
157                .map(|face| {
158                    if face.vertices.len() != 3 {
159                        return Err(Error::Type(
160                            c"Incorrectly sized array for triangle list".into(),
161                        ));
162                    }
163
164                    Ok(Triangle {
165                        first: get_point(&face.vertices[0]),
166                        second: get_point(&face.vertices[1]),
167                        third: get_point(&face.vertices[2]),
168                    })
169                })
170                .collect::<Fallible<Vec<_>>>()?;
171            Ok(MockRegion { faces, ty })
172        })
173        .collect::<Fallible<Vec<_>>>()?;
174
175    Ok(MockWorld { regions })
176}
177
178impl Convert<EntityType> for FakeXRRegionType {
179    fn convert(self) -> EntityType {
180        match self {
181            FakeXRRegionType::Point => EntityType::Point,
182            FakeXRRegionType::Plane => EntityType::Plane,
183            FakeXRRegionType::Mesh => EntityType::Mesh,
184        }
185    }
186}
187
188impl FakeXRDeviceMethods<crate::DomTypeHolder> for FakeXRDevice {
189    /// <https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md>
190    fn SetViews(
191        &self,
192        views: Vec<FakeXRViewInit>,
193        _secondary_views: Option<Vec<FakeXRViewInit>>,
194    ) -> Fallible<()> {
195        let _ = self
196            .sender
197            .send(MockDeviceMsg::SetViews(get_views(&views)?));
198        // TODO: Support setting secondary views for mock backend
199        Ok(())
200    }
201
202    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-setviewerorigin>
203    fn SetViewerOrigin(
204        &self,
205        origin: &FakeXRRigidTransformInit,
206        _emulated_position: bool,
207    ) -> Fallible<()> {
208        let _ = self
209            .sender
210            .send(MockDeviceMsg::SetViewerOrigin(Some(get_origin(origin)?)));
211        Ok(())
212    }
213
214    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-clearviewerorigin>
215    fn ClearViewerOrigin(&self) {
216        let _ = self.sender.send(MockDeviceMsg::SetViewerOrigin(None));
217    }
218
219    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-clearfloororigin>
220    fn ClearFloorOrigin(&self) {
221        let _ = self.sender.send(MockDeviceMsg::SetFloorOrigin(None));
222    }
223
224    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-setfloororigin>
225    fn SetFloorOrigin(&self, origin: &FakeXRRigidTransformInit) -> Fallible<()> {
226        let _ = self
227            .sender
228            .send(MockDeviceMsg::SetFloorOrigin(Some(get_origin(origin)?)));
229        Ok(())
230    }
231
232    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-clearworld>
233    fn ClearWorld(&self) {
234        let _ = self.sender.send(MockDeviceMsg::ClearWorld);
235    }
236
237    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-setworld>
238    fn SetWorld(&self, world: &FakeXRWorldInit) -> Fallible<()> {
239        let _ = self.sender.send(MockDeviceMsg::SetWorld(get_world(world)?));
240        Ok(())
241    }
242
243    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-simulatevisibilitychange>
244    fn SimulateVisibilityChange(&self, v: XRVisibilityState) {
245        let v = match v {
246            XRVisibilityState::Visible => Visibility::Visible,
247            XRVisibilityState::Visible_blurred => Visibility::VisibleBlurred,
248            XRVisibilityState::Hidden => Visibility::Hidden,
249        };
250        let _ = self.sender.send(MockDeviceMsg::VisibilityChange(v));
251    }
252
253    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-simulateinputsourceconnection>
254    fn SimulateInputSourceConnection(
255        &self,
256        cx: &mut JSContext,
257        init: &FakeXRInputSourceInit,
258    ) -> Fallible<DomRoot<FakeXRInputController>> {
259        let id = self.next_input_id.get();
260        self.next_input_id.set(InputId(id.0 + 1));
261
262        let handedness = init.handedness.convert();
263        let target_ray_mode = init.targetRayMode.convert();
264
265        let pointer_origin = Some(get_origin(&init.pointerOrigin)?);
266
267        let grip_origin = if let Some(ref g) = init.gripOrigin {
268            Some(get_origin(g)?)
269        } else {
270            None
271        };
272
273        let profiles = init.profiles.iter().cloned().map(String::from).collect();
274
275        let mut supported_buttons = vec![];
276        if let Some(ref buttons) = init.supportedButtons {
277            supported_buttons.extend(init_to_mock_buttons(buttons));
278        }
279
280        let source = InputSource {
281            handedness,
282            target_ray_mode,
283            id,
284            supports_grip: true,
285            profiles,
286            hand_support: None,
287        };
288
289        let init = MockInputInit {
290            source,
291            pointer_origin,
292            grip_origin,
293            supported_buttons,
294        };
295
296        let global = self.global();
297        let _ = self.sender.send(MockDeviceMsg::AddInputSource(init));
298
299        let controller = FakeXRInputController::new(cx, &global, self.sender.clone(), id);
300
301        Ok(controller)
302    }
303
304    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-disconnect>
305    fn Disconnect(&self, cx: &mut CurrentRealm) -> Rc<Promise> {
306        let global = self.global();
307        let p = Promise::new_in_realm(cx);
308        let mut trusted = Some(TrustedPromise::new(p.clone()));
309        let task_source = global
310            .task_manager()
311            .dom_manipulation_task_source()
312            .to_sendable();
313
314        let callback =
315            ProfileGenericCallback::new(global.time_profiler_chan().clone(), move |_| {
316                let trusted = trusted
317                    .take()
318                    .expect("disconnect callback called multiple times");
319                task_source.queue(trusted.resolve_task(()));
320            })
321            .expect("Could not create callback");
322        self.disconnect(callback);
323        p
324    }
325
326    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-setboundsgeometry>
327    fn SetBoundsGeometry(&self, bounds_coodinates: Vec<FakeXRBoundsPoint>) -> Fallible<()> {
328        if bounds_coodinates.len() < 3 {
329            return Err(Error::Type(
330                c"Bounds geometry must contain at least 3 points".into(),
331            ));
332        }
333        let coords = bounds_coodinates
334            .iter()
335            .map(|coord| {
336                let x = *coord.x.unwrap() as f32;
337                let y = *coord.z.unwrap() as f32;
338                Point2D::new(x, y)
339            })
340            .collect();
341        let _ = self.sender.send(MockDeviceMsg::SetBoundsGeometry(coords));
342        Ok(())
343    }
344
345    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-simulateresetpose>
346    fn SimulateResetPose(&self) {
347        let _ = self.sender.send(MockDeviceMsg::SimulateResetPose);
348    }
349}
350
351impl Convert<Handedness> for XRHandedness {
352    fn convert(self) -> Handedness {
353        match self {
354            XRHandedness::None => Handedness::None,
355            XRHandedness::Left => Handedness::Left,
356            XRHandedness::Right => Handedness::Right,
357        }
358    }
359}
360
361impl Convert<TargetRayMode> for XRTargetRayMode {
362    fn convert(self) -> TargetRayMode {
363        match self {
364            XRTargetRayMode::Gaze => TargetRayMode::Gaze,
365            XRTargetRayMode::Tracked_pointer => TargetRayMode::TrackedPointer,
366            XRTargetRayMode::Screen => TargetRayMode::Screen,
367            XRTargetRayMode::Transient_pointer => TargetRayMode::TransientPointer,
368        }
369    }
370}