molstar
Version:
A comprehensive macromolecular library.
299 lines (298 loc) • 15 kB
JavaScript
"use strict";
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.XRManager = exports.XRManagerParams = exports.DefaultXRManagerAttribs = exports.DefaultXRManagerBindings = void 0;
const rxjs_1 = require("rxjs");
const vec3_1 = require("../../mol-math/linear-algebra/3d/vec3.js");
const quat_1 = require("../../mol-math/linear-algebra/3d/quat.js");
const mat4_1 = require("../../mol-math/linear-algebra/3d/mat4.js");
const vec2_1 = require("../../mol-math/linear-algebra/3d/vec2.js");
const input_observer_1 = require("../../mol-util/input/input-observer.js");
const plane3d_1 = require("../../mol-math/geometry/primitives/plane3d.js");
const vec4_1 = require("../../mol-math/linear-algebra/3d/vec4.js");
const ray3d_1 = require("../../mol-math/geometry/primitives/ray3d.js");
const geometry_1 = require("../../mol-math/geometry.js");
const param_definition_1 = require("../../mol-util/param-definition.js");
const util_1 = require("../camera/util.js");
const binding_1 = require("../../mol-util/binding.js");
const B = input_observer_1.ButtonsType;
const Trigger = binding_1.Binding.Trigger;
const Key = binding_1.Binding.TriggerKey;
function getRigidTransformFromMat4(m) {
const d = mat4_1.Mat4.getDecomposition(m);
return new XRRigidTransform(vec3_1.Vec3.toObj(d.position), quat_1.Quat.toObj(d.quaternion));
}
function getRayFromPose(pose, view) {
const origin = vec3_1.Vec3.fromObj(pose.transform.position);
const t = mat4_1.Mat4.fromArray((0, mat4_1.Mat4)(), pose.transform.matrix, 0);
const td = mat4_1.Mat4.getDecomposition(t);
const m = mat4_1.Mat4.fromQuat((0, mat4_1.Mat4)(), td.quaternion);
const direction = vec3_1.Vec3.transformMat4((0, vec3_1.Vec3)(), vec3_1.Vec3.negUnitZ, m);
const ray = ray3d_1.Ray3D.create(origin, direction);
if (view)
ray3d_1.Ray3D.transform(ray, ray, mat4_1.Mat4.invert((0, mat4_1.Mat4)(), view));
return ray;
}
exports.DefaultXRManagerBindings = {
exit: (0, binding_1.Binding)([Key('GamepadB')]),
togglePassthrough: (0, binding_1.Binding)([Key('GamepadA')]),
gestureScale: (0, binding_1.Binding)([Trigger(B.Flag.Trigger)]),
};
exports.DefaultXRManagerAttribs = {
bindings: exports.DefaultXRManagerBindings,
};
exports.XRManagerParams = {
minTargetDistance: param_definition_1.ParamDefinition.Numeric(0.4, { min: 0.001, max: 1, step: 0.001 }),
disablePostprocessing: param_definition_1.ParamDefinition.Boolean(true),
resolutionScale: param_definition_1.ParamDefinition.Numeric(1, { min: 0.1, max: 2, step: 0.1 }),
sceneRadiusInMeters: param_definition_1.ParamDefinition.Numeric(0.25, { min: 0.01, max: 2, step: 0.01 }, { description: 'The radius of the scene bounding sphere in meters, used to set the initial camera scale.' }),
};
class XRManager {
get session() {
return this.xrSession;
}
setProps(props) {
Object.assign(this.props, props);
}
setAttribs(attribs) {
Object.assign(this.attribs, attribs);
}
intersect(camera, view, plane, targetRayPose) {
const point = (0, vec3_1.Vec3)();
const ray = getRayFromPose(targetRayPose, view);
if (plane3d_1.Plane3D.intersectRay3D(point, plane, ray)) {
const { height } = camera.viewport;
const v = (0, util_1.cameraProject)((0, vec4_1.Vec4)(), point, camera.viewport, camera.projectionView);
const screen = vec2_1.Vec2.create(Math.floor(v[0]), height - Math.floor(v[1]));
return { point, screen };
}
}
setScaleFactor(factor) {
this.scaleFactor = factor;
}
resetScale() {
this.scaleFactor = 1;
this.prevScale = 0;
}
update(xrFrame) {
var _a, _b, _c, _d, _e, _f, _g;
const { xrSession, xrRefSpace, input, camera, stereoCamera, pointerHelper } = this;
if (!xrFrame || !xrSession || !xrRefSpace)
return false;
camera.scale = camera.scale * this.scaleFactor;
this.prevScale = camera.scale;
const camDirUnscaled = vec3_1.Vec3.sub((0, vec3_1.Vec3)(), camera.position, camera.target);
vec3_1.Vec3.scaleAndAdd(camera.position, camera.position, camDirUnscaled, 1 - this.scaleFactor);
this.scaleFactor = 1;
const xform = getRigidTransformFromMat4(camera.view);
const xrOffsetRefSpace = xrRefSpace.getOffsetReferenceSpace(xform);
const xrPose = xrFrame.getViewerPose(xrOffsetRefSpace);
if (!xrPose)
return false;
const xrHeadPose = xrFrame.getViewerPose(xrRefSpace);
if (xrHeadPose) {
const hq = quat_1.Quat.fromObj(xrHeadPose.transform.orientation);
mat4_1.Mat4.fromQuat(camera.headRotation, hq);
}
const { depthFar, depthNear, baseLayer } = xrSession.renderState;
if (!baseLayer)
return false;
if (depthFar !== camera.far || depthNear !== camera.near) {
xrSession.updateRenderState({
depthNear: camera.near,
depthFar: camera.far,
});
}
stereoCamera.update({ pose: xrPose, layer: baseLayer });
const camLeft = stereoCamera.left;
const cameraTarget = vec3_1.Vec3.scale((0, vec3_1.Vec3)(), camLeft.state.target, camLeft.scale);
const cameraPosition = mat4_1.Mat4.getTranslation((0, vec3_1.Vec3)(), mat4_1.Mat4.invert((0, mat4_1.Mat4)(), camLeft.view));
const cameraDirection = vec3_1.Vec3.sub((0, vec3_1.Vec3)(), cameraPosition, cameraTarget);
const cameraPlane = plane3d_1.Plane3D.fromNormalAndCoplanarPoint((0, plane3d_1.Plane3D)(), cameraDirection, cameraTarget);
//
const pointers = [];
const points = [];
const trackedPointers = [];
const screenTouches = [];
if (xrSession.inputSources) {
for (const inputSource of xrSession.inputSources) {
if (inputSource.targetRayMode === 'screen') {
if (inputSource.gamepad) {
const { axes } = inputSource.gamepad;
const { width, height } = camLeft.viewport;
const x = ((axes[0] + 1) / 2) * width;
const y = ((axes[1] + 1) / 2) * height;
const ray = camLeft.getRay((0, ray3d_1.Ray3D)(), x, height - y);
screenTouches.push({ x, y, ray });
}
continue;
}
if (inputSource.targetRayMode !== 'tracked-pointer')
continue;
const { handedness, targetRaySpace, gamepad } = inputSource;
if (!handedness)
continue;
const targetRayPose = xrFrame.getPose(targetRaySpace, xrRefSpace);
if (!targetRayPose)
continue;
const ray = getRayFromPose(targetRayPose, camera.view);
pointers.push(ray);
const sceneBoundingSphere = geometry_1.Sphere3D.scaleNX((0, geometry_1.Sphere3D)(), this.scene.boundingSphereVisible, camLeft.scale);
const si = (0, vec3_1.Vec3)();
if (ray3d_1.Ray3D.intersectSphere3D(si, ray, sceneBoundingSphere)) {
points.push(si);
}
let buttons = input_observer_1.ButtonsType.create(input_observer_1.ButtonsType.Flag.None);
if ((_a = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[0]) === null || _a === void 0 ? void 0 : _a.pressed)
buttons |= input_observer_1.ButtonsType.Flag.Primary;
if ((_b = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[1]) === null || _b === void 0 ? void 0 : _b.pressed)
buttons |= input_observer_1.ButtonsType.Flag.Secondary;
if ((_c = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[3]) === null || _c === void 0 ? void 0 : _c.pressed)
buttons |= input_observer_1.ButtonsType.Flag.Auxilary;
if ((_d = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[4]) === null || _d === void 0 ? void 0 : _d.pressed)
buttons |= input_observer_1.ButtonsType.Flag.Forth;
if ((_e = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[5]) === null || _e === void 0 ? void 0 : _e.pressed)
buttons |= input_observer_1.ButtonsType.Flag.Five;
const prevInput = handedness === 'left' ? this.prevInput.left : this.prevInput.right;
const intersection = this.intersect(camLeft, camera.view, cameraPlane, targetRayPose);
const prevIntersection = prevInput ? this.intersect(camLeft, camera.view, cameraPlane, prevInput.targetRayPose) : undefined;
const [x, y] = (_f = intersection === null || intersection === void 0 ? void 0 : intersection.screen) !== null && _f !== void 0 ? _f : [0, 0];
const [prevX, prevY] = (_g = prevIntersection === null || prevIntersection === void 0 ? void 0 : prevIntersection.screen) !== null && _g !== void 0 ? _g : [x, y];
const dd = vec2_1.Vec2.set((0, vec2_1.Vec2)(), x - prevX, y - prevY);
vec2_1.Vec2.setMagnitude(dd, dd, Math.min(100, vec2_1.Vec2.magnitude(dd)));
const [dx, dy] = vec2_1.Vec2.round(dd, dd);
trackedPointers.push({
handedness,
buttons,
x, y, dx, dy, ray,
axes: gamepad === null || gamepad === void 0 ? void 0 : gamepad.axes
});
if (handedness === 'left') {
this.prevInput.left = { targetRayPose };
}
else {
this.prevInput.right = { targetRayPose };
}
}
}
else {
this.prevInput.left = undefined;
this.prevInput.right = undefined;
}
input.updateTrackedPointers(trackedPointers);
input.updateScreenTouches(screenTouches);
pointerHelper.ensureEnabled();
pointerHelper.update(pointers, points, this.hit);
return true;
}
async setSession(xrSession) {
if (this.xrSession === xrSession)
return;
await this.webgl.xr.set(xrSession, { resolutionScale: this.props.resolutionScale });
this.xrSession = this.webgl.xr.session;
this.prevInput = {};
this.hit = undefined;
if (this.xrSession) {
this.xrRefSpace = await this.xrSession.requestReferenceSpace('local');
this.pointerHelper.setProps({ enabled: 'on' });
let scale = this.prevScale;
if (scale === 0) {
const { radius } = this.scene.boundingSphereVisible;
scale = radius ? (1 / radius) * this.props.sceneRadiusInMeters : 0.01;
}
this.camera.forceFull = true;
this.camera.scale = scale;
this.camera.minTargetDistance = this.props.minTargetDistance;
this.prevScale = scale;
}
else {
this.xrRefSpace = undefined;
mat4_1.Mat4.setZero(this.camera.headRotation);
this.pointerHelper.setProps({ enabled: 'off' });
this.camera.forceFull = false;
this.camera.scale = 1;
this.camera.minTargetDistance = 0;
}
}
async end() {
await this.webgl.xr.end();
}
async request() {
if (!navigator.xr)
return;
const session = await navigator.xr.isSessionSupported('immersive-ar')
? await navigator.xr.requestSession('immersive-ar')
: await navigator.xr.requestSession('immersive-vr');
await this.setSession(session);
}
dispose() {
var _a;
this.hoverSub.unsubscribe();
this.keyUpSub.unsubscribe();
this.gestureSub.unsubscribe();
this.sessionChangedSub.unsubscribe();
this.togglePassthrough.complete();
this.sessionChanged.complete();
this.isSupported.complete();
(_a = navigator.xr) === null || _a === void 0 ? void 0 : _a.removeEventListener('devicechange', this.checkSupported);
}
constructor(webgl, input, scene, camera, stereoCamera, pointerHelper, interactionHelper, props = {}, attribs = {}) {
var _a;
this.webgl = webgl;
this.input = input;
this.scene = scene;
this.camera = camera;
this.stereoCamera = stereoCamera;
this.pointerHelper = pointerHelper;
this.interactionHelper = interactionHelper;
this.togglePassthrough = new rxjs_1.Subject();
this.sessionChanged = new rxjs_1.Subject();
this.isSupported = new rxjs_1.BehaviorSubject(false);
this.xrSession = undefined;
this.xrRefSpace = undefined;
this.scaleFactor = 1;
this.prevScale = 0;
this.prevInput = {};
this.hit = undefined;
this.checkSupported = async () => {
if (!navigator.xr)
return false;
const [arSupported, vrSupported] = await Promise.all([
navigator.xr.isSessionSupported('immersive-ar'),
navigator.xr.isSessionSupported('immersive-vr'),
]);
this.isSupported.next(arSupported || vrSupported);
};
this.props = { ...param_definition_1.ParamDefinition.getDefaultValues(exports.XRManagerParams), ...props };
this.attribs = { ...exports.DefaultXRManagerAttribs, ...attribs };
this.hoverSub = this.interactionHelper.events.hover.subscribe(({ position }) => {
this.hit = position;
});
this.sessionChangedSub = webgl.xr.changed.subscribe(async () => {
await this.setSession(webgl.xr.session);
this.sessionChanged.next();
});
this.checkSupported();
(_a = navigator.xr) === null || _a === void 0 ? void 0 : _a.addEventListener('devicechange', this.checkSupported);
this.keyUpSub = input.keyUp.subscribe(({ code, modifiers, key }) => {
const b = this.attribs.bindings;
if (binding_1.Binding.matchKey(b.exit, code, modifiers, key)) {
this.end();
}
if (binding_1.Binding.matchKey(b.togglePassthrough, code, modifiers, key)) {
this.togglePassthrough.next();
}
});
this.gestureSub = input.gesture.subscribe(({ scale, button, modifiers }) => {
const b = this.attribs.bindings;
if (binding_1.Binding.match(b.gestureScale, button, modifiers)) {
this.setScaleFactor(scale);
}
});
}
}
exports.XRManager = XRManager;