molstar
Version:
A comprehensive macromolecular library.
176 lines (175 loc) • 6.76 kB
JavaScript
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3.js';
import { spiral2d } from '../../mol-math/misc.js';
import { isTimingMode } from '../../mol-util/debug.js';
import { StereoCamera } from '../camera/stereo.js';
import { cameraUnproject, Viewport } from '../camera/util.js';
import { AsyncPickStatus, checkAsyncPickingSupport, PickBuffers } from '../passes/pick.js';
export class PickHelper {
setViewport(x, y, width, height) {
Viewport.set(this.viewport, x, y, width, height);
this.update();
}
setPickPadding(pickPadding) {
if (this.pickPadding !== pickPadding) {
this.pickPadding = pickPadding;
this.update();
}
}
update() {
const { x, y, width, height } = this.viewport;
this.pickRatio = this.pickPass.pickRatio;
this.pickX = Math.ceil(x * this.pickRatio);
this.pickY = Math.ceil(y * this.pickRatio);
const pickWidth = Math.floor(width * this.pickRatio);
const pickHeight = Math.floor(height * this.pickRatio);
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
this.pickWidth = pickWidth;
this.pickHeight = pickHeight;
this.halfPickWidth = Math.floor(this.pickWidth / 2);
this.buffers.setViewport(this.pickX, this.pickY, this.pickWidth, this.pickHeight);
}
this.spiral = spiral2d(Math.ceil(this.pickRatio * this.pickPadding));
this.dirty = true;
}
render(camera) {
if (isTimingMode)
this.webgl.timer.mark('PickHelper.render', { captureStats: true });
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
const { renderer, scene, helper } = this;
renderer.setTransparentBackground(false);
renderer.setDrawingBufferSize(pickWidth, pickHeight);
renderer.setPixelRatio(this.pickRatio);
if (StereoCamera.is(camera)) {
renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
this.pickPass.render(renderer, camera.left, scene, helper);
renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
this.pickPass.render(renderer, camera.right, scene, helper);
}
else {
renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
this.pickPass.render(renderer, camera, scene, helper);
}
this.dirty = false;
if (isTimingMode)
this.webgl.timer.markEnd('PickHelper.render');
}
identifyInternal(x, y, camera) {
if (this.webgl.isContextLost)
return;
const { webgl, pickRatio } = this;
if (webgl.isContextLost)
return;
x *= webgl.pixelRatio;
y *= webgl.pixelRatio;
y = this.pickPass.drawingBufferHeight - y; // flip y
const { viewport } = this;
// check if within viewport
if (x < viewport.x ||
y < viewport.y ||
x > viewport.x + viewport.width ||
y > viewport.y + viewport.height)
return;
const xv = x - viewport.x;
const yv = y - viewport.y;
const xp = Math.floor(xv * pickRatio);
const yp = Math.floor(yv * pickRatio);
const pickingId = this.buffers.getPickingId(xp, yp);
if (pickingId === undefined)
return;
const z = this.buffers.getDepth(xp, yp);
const position = Vec3.create(x, y, z);
if (StereoCamera.is(camera)) {
const halfWidth = Math.floor(viewport.width / 2);
if (x > viewport.x + halfWidth) {
position[0] = viewport.x + (xv - halfWidth) * 2;
cameraUnproject(position, position, viewport, camera.right.inverseProjectionView);
}
else {
position[0] = viewport.x + xv * 2;
cameraUnproject(position, position, viewport, camera.left.inverseProjectionView);
}
}
else {
cameraUnproject(position, position, viewport, camera.inverseProjectionView);
}
return { id: pickingId, position };
}
prepare() {
if (this.pickRatio !== this.pickPass.pickRatio) {
this.update();
}
}
getPickData(x, y, camera) {
for (const d of this.spiral) {
const pickData = this.identifyInternal(x + d[0], y + d[1], camera);
if (pickData)
return pickData;
}
}
identify(x, y, camera) {
this.prepare();
if (this.dirty) {
if (isTimingMode)
this.webgl.timer.mark('PickHelper.identify');
this.render(camera);
this.buffers.read();
if (isTimingMode)
this.webgl.timer.markEnd('PickHelper.identify');
}
return this.getPickData(x, y, camera);
}
asyncIdentify(x, y, camera) {
this.prepare();
if (this.dirty) {
if (isTimingMode)
this.webgl.timer.mark('PickHelper.asyncIdentify');
this.render(camera);
this.buffers.asyncRead();
if (isTimingMode)
this.webgl.timer.markEnd('PickHelper.asyncIdentify');
}
return {
tryGet: () => {
const status = this.buffers.check();
if (status === AsyncPickStatus.Resolved) {
return this.getPickData(x, y, camera);
}
else if (status === AsyncPickStatus.Pending) {
return 'pending';
}
else if (status === AsyncPickStatus.Failed) {
this.dirty = true;
}
}
};
}
reset() {
this.buffers.reset();
this.dirty = true;
}
dispose() {
this.buffers.dispose();
}
constructor(webgl, renderer, scene, helper, pickPass, viewport, options) {
this.webgl = webgl;
this.renderer = renderer;
this.scene = scene;
this.helper = helper;
this.pickPass = pickPass;
this.dirty = true;
this.buffers = new PickBuffers(this.webgl, this.pickPass);
this.viewport = Viewport();
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
this.pickPadding = options.pickPadding;
if (!checkAsyncPickingSupport(webgl)) {
this.asyncIdentify = (x, y, camera) => ({
tryGet: () => this.identify(x, y, camera)
});
}
}
}