UNPKG

molstar

Version:

A comprehensive macromolecular library.

299 lines (298 loc) 15 kB
"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;