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