molstar
Version:
A comprehensive macromolecular library.
295 lines (294 loc) • 13.9 kB
JavaScript
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { BehaviorSubject, Subject } from 'rxjs';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3.js';
import { Quat } from '../../mol-math/linear-algebra/3d/quat.js';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4.js';
import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2.js';
import { ButtonsType } from '../../mol-util/input/input-observer.js';
import { Plane3D } from '../../mol-math/geometry/primitives/plane3d.js';
import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4.js';
import { Ray3D } from '../../mol-math/geometry/primitives/ray3d.js';
import { Sphere3D } from '../../mol-math/geometry.js';
import { ParamDefinition as PD } from '../../mol-util/param-definition.js';
import { cameraProject } from '../camera/util.js';
import { Binding } from '../../mol-util/binding.js';
const B = ButtonsType;
const Trigger = Binding.Trigger;
const Key = Binding.TriggerKey;
function getRigidTransformFromMat4(m) {
const d = Mat4.getDecomposition(m);
return new XRRigidTransform(Vec3.toObj(d.position), Quat.toObj(d.quaternion));
}
function getRayFromPose(pose, view) {
const origin = Vec3.fromObj(pose.transform.position);
const t = Mat4.fromArray(Mat4(), pose.transform.matrix, 0);
const td = Mat4.getDecomposition(t);
const m = Mat4.fromQuat(Mat4(), td.quaternion);
const direction = Vec3.transformMat4(Vec3(), Vec3.negUnitZ, m);
const ray = Ray3D.create(origin, direction);
if (view)
Ray3D.transform(ray, ray, Mat4.invert(Mat4(), view));
return ray;
}
export const DefaultXRManagerBindings = {
exit: Binding([Key('GamepadB')]),
togglePassthrough: Binding([Key('GamepadA')]),
gestureScale: Binding([Trigger(B.Flag.Trigger)]),
};
export const DefaultXRManagerAttribs = {
bindings: DefaultXRManagerBindings,
};
export const XRManagerParams = {
minTargetDistance: PD.Numeric(0.4, { min: 0.001, max: 1, step: 0.001 }),
disablePostprocessing: PD.Boolean(true),
resolutionScale: PD.Numeric(1, { min: 0.1, max: 2, step: 0.1 }),
sceneRadiusInMeters: PD.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.' }),
};
export 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 = Vec3();
const ray = getRayFromPose(targetRayPose, view);
if (Plane3D.intersectRay3D(point, plane, ray)) {
const { height } = camera.viewport;
const v = cameraProject(Vec4(), point, camera.viewport, camera.projectionView);
const screen = 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.sub(Vec3(), camera.position, camera.target);
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.fromObj(xrHeadPose.transform.orientation);
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.scale(Vec3(), camLeft.state.target, camLeft.scale);
const cameraPosition = Mat4.getTranslation(Vec3(), Mat4.invert(Mat4(), camLeft.view));
const cameraDirection = Vec3.sub(Vec3(), cameraPosition, cameraTarget);
const cameraPlane = Plane3D.fromNormalAndCoplanarPoint(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(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 = Sphere3D.scaleNX(Sphere3D(), this.scene.boundingSphereVisible, camLeft.scale);
const si = Vec3();
if (Ray3D.intersectSphere3D(si, ray, sceneBoundingSphere)) {
points.push(si);
}
let buttons = ButtonsType.create(ButtonsType.Flag.None);
if ((_a = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[0]) === null || _a === void 0 ? void 0 : _a.pressed)
buttons |= ButtonsType.Flag.Primary;
if ((_b = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[1]) === null || _b === void 0 ? void 0 : _b.pressed)
buttons |= ButtonsType.Flag.Secondary;
if ((_c = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[3]) === null || _c === void 0 ? void 0 : _c.pressed)
buttons |= ButtonsType.Flag.Auxilary;
if ((_d = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[4]) === null || _d === void 0 ? void 0 : _d.pressed)
buttons |= ButtonsType.Flag.Forth;
if ((_e = gamepad === null || gamepad === void 0 ? void 0 : gamepad.buttons[5]) === null || _e === void 0 ? void 0 : _e.pressed)
buttons |= 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.set(Vec2(), x - prevX, y - prevY);
Vec2.setMagnitude(dd, dd, Math.min(100, Vec2.magnitude(dd)));
const [dx, dy] = 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.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 Subject();
this.sessionChanged = new Subject();
this.isSupported = new 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 = { ...PD.getDefaultValues(XRManagerParams), ...props };
this.attribs = { ...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.matchKey(b.exit, code, modifiers, key)) {
this.end();
}
if (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.match(b.gestureScale, button, modifiers)) {
this.setScaleFactor(scale);
}
});
}
}