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