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