UNPKG

molstar

Version:

A comprehensive macromolecular library.

295 lines (294 loc) 13.9 kB
/** * 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); } }); } }