UNPKG

molstar

Version:

A comprehensive macromolecular library.

1,028 lines (1,027 loc) 61.5 kB
"use strict"; /** * Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> * @author Gianluca Tomasello <giagitom@gmail.com> * @author Herman Bergwerf <post@hbergwerf.nl> */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Canvas3D = exports.Canvas3DContext = exports.DefaultCanvas3DAttribs = exports.DefaultCanvas3DParams = exports.Canvas3DParams = exports.CameraFogParams = void 0; const rxjs_1 = require("rxjs"); const now_1 = require("../mol-util/now.js"); const linear_algebra_1 = require("../mol-math/linear-algebra.js"); const input_observer_1 = require("../mol-util/input/input-observer.js"); const renderer_1 = require("../mol-gl/renderer.js"); const trackball_1 = require("./controls/trackball.js"); const util_1 = require("./camera/util.js"); const context_1 = require("../mol-gl/webgl/context.js"); const representation_1 = require("../mol-repr/representation.js"); const scene_1 = require("../mol-gl/scene.js"); const loci_1 = require("../mol-model/loci.js"); const camera_1 = require("./camera.js"); const param_definition_1 = require("../mol-util/param-definition.js"); const bounding_sphere_helper_1 = require("./helper/bounding-sphere-helper.js"); const set_1 = require("../mol-util/set.js"); const interaction_events_1 = require("./helper/interaction-events.js"); const postprocessing_1 = require("./passes/postprocessing.js"); const multi_sample_1 = require("./passes/multi-sample.js"); const pick_1 = require("./passes/pick.js"); const pick_helper_1 = require("./helper/pick-helper.js"); const image_1 = require("./passes/image.js"); const geometry_1 = require("../mol-math/geometry.js"); const debug_1 = require("../mol-util/debug.js"); const camera_helper_1 = require("./helper/camera-helper.js"); const handle_helper_1 = require("./helper/handle-helper.js"); const stereo_1 = require("./camera/stereo.js"); const helper_1 = require("./helper/helper.js"); const passes_1 = require("./passes/passes.js"); const mol_util_1 = require("../mol-util/index.js"); const marking_1 = require("./passes/marking.js"); const misc_1 = require("../mol-math/misc.js"); const object_1 = require("../mol-util/object.js"); const hi_z_1 = require("./passes/hi-z.js"); const illumination_1 = require("./passes/illumination.js"); const browser_1 = require("../mol-util/browser.js"); const pointer_helper_1 = require("./helper/pointer-helper.js"); const xr_manager_1 = require("./helper/xr-manager.js"); const ray_helper_1 = require("./helper/ray-helper.js"); const produce_1 = require("../mol-util/produce.js"); const shader_manager_1 = require("./helper/shader-manager.js"); const number_1 = require("../mol-util/number.js"); exports.CameraFogParams = { intensity: param_definition_1.ParamDefinition.Numeric(15, { min: 1, max: 100, step: 1 }), }; exports.Canvas3DParams = { camera: param_definition_1.ParamDefinition.Group({ mode: param_definition_1.ParamDefinition.Select('perspective', param_definition_1.ParamDefinition.arrayToOptions(['perspective', 'orthographic']), { label: 'Camera' }), helper: param_definition_1.ParamDefinition.Group(camera_helper_1.CameraHelperParams, { isFlat: true }), stereo: param_definition_1.ParamDefinition.MappedStatic('off', { on: param_definition_1.ParamDefinition.Group(stereo_1.StereoCameraParams), off: param_definition_1.ParamDefinition.Group({}) }, { cycle: true, hideIf: p => (p === null || p === void 0 ? void 0 : p.mode) !== 'perspective' }), fov: param_definition_1.ParamDefinition.Numeric(45, { min: 10, max: 130, step: 1 }, { label: 'Field of View' }), manualReset: param_definition_1.ParamDefinition.Boolean(false, { isHidden: true }), }, { pivot: 'mode' }), cameraFog: param_definition_1.ParamDefinition.MappedStatic('on', { on: param_definition_1.ParamDefinition.Group(exports.CameraFogParams), off: param_definition_1.ParamDefinition.Group({}) }, { cycle: true, description: 'Show fog in the distance' }), cameraClipping: param_definition_1.ParamDefinition.Group({ radius: param_definition_1.ParamDefinition.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }), far: param_definition_1.ParamDefinition.Boolean(true, { description: 'Hide scene in the distance' }), minNear: param_definition_1.ParamDefinition.Numeric(1, { min: 0.1, max: 100, step: 0.1 }, { description: 'Minimal allowed distance of near clipping plane from the camera. Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }), }, { pivot: 'radius' }), viewport: param_definition_1.ParamDefinition.MappedStatic('canvas', { canvas: param_definition_1.ParamDefinition.Group({}), 'static-frame': param_definition_1.ParamDefinition.Group({ x: param_definition_1.ParamDefinition.Numeric(0), y: param_definition_1.ParamDefinition.Numeric(0), width: param_definition_1.ParamDefinition.Numeric(128), height: param_definition_1.ParamDefinition.Numeric(128) }), 'relative-frame': param_definition_1.ParamDefinition.Group({ x: param_definition_1.ParamDefinition.Numeric(0.33, { min: 0, max: 1, step: 0.01 }), y: param_definition_1.ParamDefinition.Numeric(0.33, { min: 0, max: 1, step: 0.01 }), width: param_definition_1.ParamDefinition.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 }), height: param_definition_1.ParamDefinition.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 }) }) }), cameraResetDurationMs: param_definition_1.ParamDefinition.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }), sceneRadiusFactor: param_definition_1.ParamDefinition.Numeric(1, { min: 1, max: 10, step: 0.1 }), transparentBackground: param_definition_1.ParamDefinition.Boolean(false), checkeredTransparentBackground: param_definition_1.ParamDefinition.Boolean(false), dpoitIterations: param_definition_1.ParamDefinition.Numeric(2, { min: 1, max: 10, step: 1 }), pickPadding: param_definition_1.ParamDefinition.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }), userInteractionReleaseMs: param_definition_1.ParamDefinition.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }), multiSample: param_definition_1.ParamDefinition.Group(multi_sample_1.MultiSampleParams), postprocessing: param_definition_1.ParamDefinition.Group(postprocessing_1.PostprocessingParams), marking: param_definition_1.ParamDefinition.Group(marking_1.MarkingParams), illumination: param_definition_1.ParamDefinition.Group(illumination_1.IlluminationParams), hiZ: param_definition_1.ParamDefinition.Group(hi_z_1.HiZParams), renderer: param_definition_1.ParamDefinition.Group(renderer_1.RendererParams), trackball: param_definition_1.ParamDefinition.Group(trackball_1.TrackballControlsParams), interaction: param_definition_1.ParamDefinition.Group(interaction_events_1.Canvas3dInteractionHelperParams), debug: param_definition_1.ParamDefinition.Group(bounding_sphere_helper_1.DebugHelperParams), handle: param_definition_1.ParamDefinition.Group(handle_helper_1.HandleHelperParams), pointer: param_definition_1.ParamDefinition.Group(pointer_helper_1.PointerHelperParams), xr: param_definition_1.ParamDefinition.Group(xr_manager_1.XRManagerParams, { label: 'XR' }), }; exports.DefaultCanvas3DParams = param_definition_1.ParamDefinition.getDefaultValues(exports.Canvas3DParams); exports.DefaultCanvas3DAttribs = { trackball: trackball_1.DefaultTrackballControlsAttribs, xr: xr_manager_1.DefaultXRManagerAttribs, }; var Canvas3DContext; (function (Canvas3DContext) { Canvas3DContext.DefaultAttribs = { powerPreference: 'high-performance', failIfMajorPerformanceCaveat: false, /** true by default to avoid issues with Safari (Jan 2021) */ antialias: true, /** true to support multiple Canvas3D objects with a single context */ preserveDrawingBuffer: true, preferWebGl1: false, handleResize: () => { }, }; Canvas3DContext.Params = { resolutionMode: param_definition_1.ParamDefinition.Select('auto', param_definition_1.ParamDefinition.arrayToOptions(['auto', 'scaled', 'native'])), pixelScale: param_definition_1.ParamDefinition.Numeric(1, { min: 0.1, max: 2, step: 0.05 }), pickScale: param_definition_1.ParamDefinition.Numeric(0.25, { min: 0.1, max: 1, step: 0.05 }), transparency: param_definition_1.ParamDefinition.Select('wboit', [['blended', 'Blended'], ['wboit', 'Weighted, Blended'], ['dpoit', 'Depth Peeling']]), }; Canvas3DContext.DefaultProps = param_definition_1.ParamDefinition.getDefaultValues(Canvas3DContext.Params); function fromCanvas(canvas, assetManager, attribs = {}, props = {}) { const a = { ...Canvas3DContext.DefaultAttribs, ...attribs }; const p = { ...Canvas3DContext.DefaultProps, ...props }; const { powerPreference, failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, preferWebGl1 } = a; const gl = (0, context_1.getGLContext)(canvas, { powerPreference, failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, alpha: true, // the renderer requires an alpha channel depth: true, // the renderer requires a depth buffer premultipliedAlpha: true, // the renderer outputs PMA preferWebGl1 }); if (gl === null) throw new Error('Could not create a WebGL rendering context'); const getPixelScale = () => { const scaled = (p.pixelScale / (typeof window !== 'undefined' ? ((window === null || window === void 0 ? void 0 : window.devicePixelRatio) || 1) : 1)); if (p.resolutionMode === 'auto') { return (0, browser_1.isMobileBrowser)() ? scaled : p.pixelScale; } return p.resolutionMode === 'native' ? p.pixelScale : scaled; }; const syncPixelScale = () => { const pixelScale = getPixelScale(); input.setPixelScale(pixelScale); webgl.setPixelScale(pixelScale); }; const { pickScale, transparency } = p; const pixelScale = getPixelScale(); const input = input_observer_1.InputObserver.fromElement(canvas, { pixelScale, preventGestures: true }); const webgl = (0, context_1.createContext)(gl, { pixelScale }); const passes = new passes_1.Passes(webgl, assetManager, { pickScale, transparency }); if (debug_1.isDebugMode) { const loseContextExt = gl.getExtension('WEBGL_lose_context'); if (loseContextExt) { // Hold down shift+ctrl+alt and press any mouse button to call `loseContext`. // After 1 second `restoreContext` will be called. canvas.addEventListener('mousedown', e => { if (webgl.isContextLost) return; if (!e.shiftKey || !e.ctrlKey || !e.altKey) return; if (debug_1.isDebugMode) console.log('lose context'); loseContextExt.loseContext(); setTimeout(() => { if (!webgl.isContextLost) return; if (debug_1.isDebugMode) console.log('restore context'); loseContextExt.restoreContext(); }, 1000); }, false); } } // https://www.khronos.org/webgl/wiki/HandlingContextLost const contextLost = new rxjs_1.Subject(); const handleWebglContextLost = (e) => { webgl.setContextLost(); e.preventDefault(); if (debug_1.isDebugMode) console.log('context lost'); contextLost.next((0, now_1.now)()); }; const handlewWebglContextRestored = () => { if (!webgl.isContextLost) return; webgl.handleContextRestored(() => { passes.draw.reset(); passes.pick.reset(); passes.illumination.reset(); }); if (debug_1.isDebugMode) console.log('context restored'); }; canvas.addEventListener('webglcontextlost', handleWebglContextLost, false); canvas.addEventListener('webglcontextrestored', handlewWebglContextRestored, false); const changed = new rxjs_1.BehaviorSubject(undefined); return { canvas, webgl, input, passes, attribs: a, get props() { return { ...p }; }, contextLost, contextRestored: webgl.contextRestored, assetManager, changed, get pixelScale() { return getPixelScale(); }, syncPixelScale, setProps: (props) => { if (!props) return; let hasChanged = false; let pixelScaleNeedsUpdate = false; if (props.resolutionMode !== undefined && props.resolutionMode !== p.resolutionMode) { p.resolutionMode = props.resolutionMode; pixelScaleNeedsUpdate = true; } if (props.pixelScale !== undefined && props.pixelScale !== p.pixelScale) { p.pixelScale = props.pixelScale; pixelScaleNeedsUpdate = true; } if (pixelScaleNeedsUpdate) { syncPixelScale(); a.handleResize(); hasChanged = true; } if (props.pickScale !== undefined && props.pickScale !== p.pickScale) { p.pickScale = props.pickScale; passes.setPickScale(props.pickScale); hasChanged = true; } if (props.transparency !== undefined && props.transparency !== p.transparency) { p.transparency = props.transparency; passes.setTransparency(props.transparency); hasChanged = true; } if (hasChanged) changed.next(undefined); }, dispose: (options) => { input.dispose(); canvas.removeEventListener('webglcontextlost', handleWebglContextLost, false); canvas.removeEventListener('webglcontextrestored', handlewWebglContextRestored, false); webgl.destroy(options); contextLost.complete(); changed.complete(); } }; } Canvas3DContext.fromCanvas = fromCanvas; })(Canvas3DContext || (exports.Canvas3DContext = Canvas3DContext = {})); const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f) => setImmediate(() => f(Date.now())); const cancelAnimationFrame = typeof window !== 'undefined' ? window.cancelAnimationFrame : (handle) => clearImmediate(handle); function syncCanvasBackground(canvas, canvasProps) { if (canvasProps.transparentBackground && canvasProps.checkeredTransparentBackground) { Object.assign(canvas.style, { 'background-image': 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey), linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey)', 'background-size': '60px 60px', 'background-position': '0 0, 30px 30px' }); } else { Object.assign(canvas.style, { 'background-image': '', 'background-size': '', 'background-position': '' }); } } var Canvas3D; (function (Canvas3D) { function create(ctx, props = {}, attribs = {}) { var _a, _b; const { webgl, input, passes, assetManager, canvas, contextLost } = ctx; const p = { ...(0, object_1.deepClone)(exports.DefaultCanvas3DParams), ...(0, object_1.deepClone)(props) }; const a = { ...(0, object_1.deepClone)(exports.DefaultCanvas3DAttribs), ...(0, object_1.deepClone)(attribs) }; const reprRenderObjects = new Map(); const reprUpdatedSubscriptions = new Map(); const reprCount = new rxjs_1.BehaviorSubject(0); const interactionEvent = new rxjs_1.Subject(); let startTime = (0, now_1.now)(); const didDraw = new rxjs_1.BehaviorSubject(0); const commited = new rxjs_1.BehaviorSubject(0); const commitQueueSize = new rxjs_1.BehaviorSubject(0); const { contextRestored } = webgl; let x = 0; let y = 0; let width = 128; let height = 128; let forceNextRender = false; let currentTime = 0; syncCanvasBackground(canvas, p); updateViewport(); const scene = scene_1.Scene.create(webgl, passes.draw.transparency, { dColorMarker: p.renderer.colorMarker, dLightCount: (_a = p.renderer.light) === null || _a === void 0 ? void 0 : _a.length, }); function getSceneRadius() { return scene.boundingSphere.radius * p.sceneRadiusFactor; } const camera = new camera_1.Camera({ position: linear_algebra_1.Vec3.create(0, 0, 100), mode: p.camera.mode, fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0, clipFar: p.cameraClipping.far, minNear: p.cameraClipping.minNear, fov: (0, misc_1.degToRad)(p.camera.fov), }, { x, y, width, height }); const stereoCamera = new stereo_1.StereoCamera(camera, p.camera.stereo.params); const controls = trackball_1.TrackballControls.create(input, camera, scene, p.trackball, a.trackball); const helper = new helper_1.Helper(webgl, scene, p); const hiZ = new hi_z_1.HiZPass(webgl, passes.draw, canvas, p.hiZ); const renderer = renderer_1.Renderer.create(webgl, p.renderer); renderer.setOcclusionTest(hiZ.isOccluded); const shaderManager = new shader_manager_1.ShaderManager(webgl, scene); shaderManager.updateRequired(p); const pickOptions = { pickPadding: p.pickPadding, maxAsyncReadLag: pick_1.DefaultPickOptions.maxAsyncReadLag, }; const pickHelper = new pick_helper_1.PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, pickOptions); const rayHelper = new ray_helper_1.RayHelper(webgl, renderer, scene, helper, pickOptions); const interactionHelper = new interaction_events_1.Canvas3dInteractionHelper(identify, asyncIdentify, getLoci, input, camera, controls, p.interaction); const multiSampleHelper = new multi_sample_1.MultiSampleHelper(passes.multiSample); passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => { if (changed) requestDraw(); }); let cameraResetRequested = false; let nextCameraResetDuration = void 0; let nextCameraResetSnapshot = void 0; let resizeRequested = false; // function getNonXRProps() { return { transparency: ctx.props.transparency, transparentBackground: p.transparentBackground, hiZ: hiZ.props.enabled, postprocessing: p.postprocessing.enabled, axes: (0, object_1.deepClone)(helper.camera.props.axes), }; } const nonXRProps = getNonXRProps(); function saveNonXRProps() { Object.assign(nonXRProps, getNonXRProps()); } function loadNonXRProps() { p.postprocessing.enabled = nonXRProps.postprocessing; p.transparentBackground = nonXRProps.transparentBackground; ctx.setProps({ transparency: nonXRProps.transparency }); hiZ.setProps({ enabled: nonXRProps.hiZ }); helper.camera.setProps({ axes: nonXRProps.axes }); } function setXRProps() { var _a; p.postprocessing.enabled = !xrManager.props.disablePostprocessing; ctx.setProps({ transparency: 'blended' }); hiZ.setProps({ enabled: false }); helper.camera.setProps({ axes: { name: 'off', params: {} } }); if (((_a = xrManager.session) === null || _a === void 0 ? void 0 : _a.environmentBlendMode) === 'alpha-blend') { p.transparentBackground = xrPassthrough; } } const xrManager = new xr_manager_1.XRManager(webgl, input, scene, camera, stereoCamera, helper.pointer, interactionHelper, p.xr, a.xr); const xr = { request: async () => { try { await xrManager.request(); } catch (e) { console.error(e); xr.requestFailed.next(e); } }, end: () => xrManager.end(), isSupported: new rxjs_1.BehaviorSubject(false), isPresenting: new rxjs_1.BehaviorSubject(false), requestFailed: new rxjs_1.Subject(), }; let xrPassthrough = false; const xrSubs = [ xrManager.isSupported.subscribe(e => xr.isSupported.next(e)), xrManager.togglePassthrough.subscribe(() => { var _a; if (((_a = xrManager.session) === null || _a === void 0 ? void 0 : _a.environmentBlendMode) === 'alpha-blend') { xrPassthrough = !p.transparentBackground; } }), xrManager.sessionChanged.subscribe(() => { var _a; resizeRequested = true; if (xrManager.session) { saveNonXRProps(); xrPassthrough = ((_a = xrManager.session) === null || _a === void 0 ? void 0 : _a.environmentBlendMode) === 'alpha-blend'; setXRProps(); } else { loadNonXRProps(); } resume(); xr.isPresenting.next(!!xrManager.session); }), ]; // let notifyDidDraw = true; function getLoci(pickingId) { let loci = loci_1.EmptyLoci; let repr = representation_1.Representation.Empty; if (pickingId) { const cameraHelperLoci = helper.camera.getLoci(pickingId); if (cameraHelperLoci !== loci_1.EmptyLoci) return { loci: cameraHelperLoci, repr }; loci = helper.handle.getLoci(pickingId); reprRenderObjects.forEach((_, _repr) => { const _loci = _repr.getLoci(pickingId); if (!(0, loci_1.isEmptyLoci)(_loci)) { if (!(0, loci_1.isEmptyLoci)(loci)) { console.warn('found another loci, this should not happen'); } loci = _loci; repr = _repr; } }); } return { loci, repr }; } let markBuffer = []; function mark(reprLoci, action) { // NOTE: might try to optimize a case with opposite actions for the // same loci. Tho this might end up being more expensive (and error prone) // then just applying everything "naively". markBuffer.push([reprLoci, action]); } function resolveMarking() { let changed = false; for (const [r, l] of markBuffer) { changed = applyMark(r, l) || changed; } markBuffer = []; if (changed) { scene.update(void 0, true); helper.handle.scene.update(void 0, true); helper.camera.scene.update(void 0, true); shaderManager.updateRequired(p); shaderManager.finalizeRequired(true); interactionEvent.next(); } return changed; } function applyMark(reprLoci, action) { const { repr, loci } = reprLoci; let changed = false; if (repr) { changed = repr.mark(loci, action) || changed; } else { reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; }); } changed = helper.handle.mark(loci, action) || changed; changed = helper.camera.mark(loci, action) || changed; return changed; } function render(force, xrFrame) { if (webgl.isContextLost) return false; if (webgl.xr.session && !xrFrame) return false; let resized = false; if (resizeRequested) { handleResize(false); resizeRequested = false; resized = true; } const drs = webgl.getDrawingBufferSize(); if (x > drs.width || x + width < 0 || y > drs.height || y + height < 0) return false; if (xrFrame) { setXRProps(); p.transparentBackground = xrPassthrough; } const markingUpdated = resolveMarking() && (renderer.props.colorMarker || p.marking.enabled); let didRender = false; controls.update(currentTime); const cameraChanged = camera.update(); const xrChanged = xrManager.update(xrFrame); if (!xrChanged && xrFrame) return false; const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged; forceNextRender = false; if (passes.illumination.supported && p.illumination.enabled && !xrFrame) { if (shouldRender || markingUpdated) { renderer.setOcclusionTest(null); passes.illumination.restart(); } if (passes.illumination.shouldRender(p.illumination) && ((!isActivelyInteracting && scene.count > 0) || passes.illumination.iteration === 0 || p.userInteractionReleaseMs === 0)) { if (debug_1.isTimingMode) webgl.timer.mark('Canvas3D.render', { captureStats: true }); const ctx = { renderer, camera, scene, helper }; passes.illumination.render(ctx, p, true); if (debug_1.isTimingMode) webgl.timer.markEnd('Canvas3D.render'); // if only marking has updated, do not set the flag to dirty pickHelper.dirty = pickHelper.dirty || shouldRender; didRender = true; } } else { const multiSampleChanged = multiSampleHelper.update(markingUpdated || shouldRender, p.multiSample) && !xrFrame; if (shouldRender || multiSampleChanged || markingUpdated) { renderer.setOcclusionTest(hiZ.isOccluded); let cam = camera; if (p.camera.stereo.name === 'on' || xrChanged) { if (!xrChanged) stereoCamera.update(); cam = stereoCamera; } if (debug_1.isTimingMode) webgl.timer.mark('Canvas3D.render', { captureStats: true }); const ctx = { renderer, camera: cam, scene, helper }; if (multi_sample_1.MultiSamplePass.isEnabled(p.multiSample) && !xrFrame) { const forceOn = p.multiSample.reduceFlicker && !cameraChanged && markingUpdated && !controls.isAnimating; multiSampleHelper.render(ctx, p, true, forceOn); } else { passes.draw.render(ctx, p, true); } hiZ.render(camera); if (debug_1.isTimingMode) webgl.timer.markEnd('Canvas3D.render'); // if only marking has updated, do not set the flag to dirty pickHelper.dirty = pickHelper.dirty || shouldRender; didRender = true; } } return didRender; } let forceDrawAfterAllCommited = false; let drawPaused = false; let isContextLost = false; function draw(options) { if (drawPaused || isContextLost) return; if (!shaderManager.finalizeRequired(options === null || options === void 0 ? void 0 : options.isSynchronous)) { forceNextRender = true; return; } if (render(!!(options === null || options === void 0 ? void 0 : options.force), options === null || options === void 0 ? void 0 : options.xrFrame) && notifyDidDraw) { didDraw.next((0, now_1.now)() - startTime); } } function requestDraw() { forceNextRender = true; } let animationFrameHandle = 0; function tick(t, options) { if (isContextLost) return; if (webgl.xr.session && !(options === null || options === void 0 ? void 0 : options.xrFrame)) return; currentTime = t; commit(options === null || options === void 0 ? void 0 : options.isSynchronous); // update the controler before the camera transition if (options === null || options === void 0 ? void 0 : options.updateControls) { controls.update(currentTime); } camera.transition.tick(currentTime); hiZ.tick(); if (options === null || options === void 0 ? void 0 : options.manualDraw) { return; } draw({ isSynchronous: options === null || options === void 0 ? void 0 : options.isSynchronous, xrFrame: options === null || options === void 0 ? void 0 : options.xrFrame }); if (!camera.transition.inTransition && !webgl.isContextLost) { interactionHelper.tick(currentTime); } } let animationFrameCB = undefined; function _requestAnimationFrame(callback) { animationFrameCB = callback; return webgl.xr.session ? webgl.xr.session.requestAnimationFrame(callback) : requestAnimationFrame(callback); } function _cancelAnimationFrame(handle) { animationFrameCB = undefined; webgl.xr.session ? webgl.xr.session.cancelAnimationFrame(handle) : cancelAnimationFrame(handle); } function _animate(_timestamp, xrFrame) { tick((0, now_1.now)(), { xrFrame }); animationFrameHandle = _requestAnimationFrame(_animate); } function resetTime(t) { startTime = t; controls.start(t); } function animate() { drawPaused = false; controls.start((0, now_1.now)()); if (animationFrameHandle === 0) _animate(0); } function pause(noDraw = false) { drawPaused = noDraw; if (animationFrameHandle !== 0) { _cancelAnimationFrame(animationFrameHandle); animationFrameHandle = 0; } } function resume() { drawPaused = false; if (animationFrameCB) _requestAnimationFrame(animationFrameCB); } function identify(target) { if (webgl.isContextLost) return undefined; shaderManager.finalize(['pick'], true); if ('origin' in target) { return rayHelper.identify(target, camera); } else { const cam = (p.camera.stereo.name === 'on') ? stereoCamera : camera; return pickHelper.identify(target[0], target[1], cam); } } function asyncIdentify(target) { if (webgl.isContextLost) return undefined; shaderManager.finalize(['pick'], true); if ('origin' in target) { return rayHelper.asyncIdentify(target, camera); } else { const cam = (p.camera.stereo.name === 'on') ? stereoCamera : camera; return pickHelper.asyncIdentify(target[0], target[1], cam); } } function commit(isSynchronous = false) { const allCommited = commitScene(isSynchronous); shaderManager.updateRequired(p); // Only reset the camera after the full scene has been commited. if (allCommited) { resolveCameraReset(); if (forceDrawAfterAllCommited) { if (helper.debug.isEnabled) helper.debug.update(); draw({ force: true }); forceDrawAfterAllCommited = false; } commited.next((0, now_1.now)()); } } function resolveCameraReset() { if (!cameraResetRequested) return; if (!xr.isPresenting.value) { xrManager.resetScale(); } const boundingSphere = scene.boundingSphereVisible; const { center, radius } = boundingSphere; const autoAdjustControls = controls.props.autoAdjustMinMaxDistance; if (autoAdjustControls.name === 'on') { const minDistance = autoAdjustControls.params.minDistanceFactor * radius + autoAdjustControls.params.minDistancePadding; const maxDistance = Math.max(autoAdjustControls.params.maxDistanceFactor * radius, autoAdjustControls.params.maxDistanceMin); controls.setProps({ minDistance, maxDistance }); } if (radius > 0) { const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration; const focus = camera.getFocus(center, radius); const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot; const snapshot = next ? { ...focus, ...next } : focus; camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration); } nextCameraResetDuration = void 0; nextCameraResetSnapshot = void 0; cameraResetRequested = false; } const oldBoundingSphereVisible = (0, geometry_1.Sphere3D)(); const cameraSphere = (0, geometry_1.Sphere3D)(); function shouldResetCamera() { if (camera.state.radiusMax === 0) return true; if (camera.transition.inTransition || nextCameraResetSnapshot) return false; let cameraSphereOverlapsNone = true, isEmpty = true; geometry_1.Sphere3D.set(cameraSphere, camera.state.target, camera.state.radius); // check if any renderable has moved outside of the old bounding sphere // and if no renderable is overlapping with the camera sphere for (const r of scene.renderables) { if (!r.state.visible) continue; const b = r.values.boundingSphere.ref.value; if (!b.radius) continue; isEmpty = false; const cameraDist = linear_algebra_1.Vec3.distance(cameraSphere.center, b.center); if ((cameraDist > cameraSphere.radius || cameraDist > b.radius || b.radius > camera.state.radiusMax) && !geometry_1.Sphere3D.includes(oldBoundingSphereVisible, b)) return true; if (geometry_1.Sphere3D.overlaps(cameraSphere, b)) cameraSphereOverlapsNone = false; } return cameraSphereOverlapsNone || (!isEmpty && cameraSphere.radius <= 0.1); } const sceneCommitTimeoutMs = 250; function commitScene(isSynchronous) { if (!scene.needsCommit) return true; // snapshot the current bounding sphere of visible objects geometry_1.Sphere3D.copy(oldBoundingSphereVisible, scene.boundingSphereVisible); // clear hi-Z buffer when scene changes hiZ.clear(); if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) { commitQueueSize.next(scene.commitQueueSize); return false; } commitQueueSize.next(0); if (helper.debug.isEnabled) helper.debug.update(); if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) { cameraResetRequested = true; } if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0; if (!p.camera.manualReset) camera.setState({ radiusMax: getSceneRadius() }, 0); reprCount.next(reprRenderObjects.size); if (debug_1.isDebugMode) consoleStats(); return true; } function consoleStats() { const items = scene.renderables.map(r => ({ drawCount: r.values.drawCount.ref.value, instanceCount: r.values.instanceCount.ref.value, materialId: r.materialId, renderItemId: r.id, geometryType: r.values.dGeometryType.ref.value, 'byteCount [MiB]': (0, number_1.toFixed)(r.getByteCount() / 1024 / 1024, 3), })); console.groupCollapsed(`${items.length} RenderItems`); if (items.length <= 64) { console.table(items); } else { console.log(items); } console.log(JSON.stringify(webgl.stats, undefined, 4)); const { texture, cubeTexture, attribute, elements, pixelPack, renderbuffer } = webgl.resources.getByteCounts(); console.log(JSON.stringify({ texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`, cubeTexture: `${(cubeTexture / 1024 / 1024).toFixed(3)} MiB`, attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`, elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`, pixelPack: `${(pixelPack / 1024 / 1024).toFixed(3)} MiB`, renderbuffer: `${(renderbuffer / 1024 / 1024).toFixed(3)} MiB`, }, undefined, 4)); console.log(JSON.stringify({ renderables: `${(scene.renderables.reduce((sum, r) => sum + r.getByteCount(), 0) / 1024 / 1024).toFixed(3)} MiB`, passes: { draw: `${(passes.draw.getByteCount() / 1024 / 1024).toFixed(3)} MiB`, illumination: `${(passes.illumination.getByteCount() / 1024 / 1024).toFixed(3)} MiB`, pick: `${(passes.pick.getByteCount() / 1024 / 1024).toFixed(3)} MiB`, hiZ: `${(hiZ.getByteCount() / 1024 / 1024).toFixed(3)} MiB`, } }, undefined, 4)); if (debug_1.isTimingMode) { console.log(JSON.stringify(webgl.timer.formatedStats(), undefined, 4)); } console.groupEnd(); } function add(repr) { registerAutoUpdate(repr); const oldRO = reprRenderObjects.get(repr); const newRO = new Set(); repr.renderObjects.forEach(o => newRO.add(o)); if (oldRO) { if (!set_1.SetUtils.areEqual(newRO, oldRO)) { newRO.forEach(o => { if (!oldRO.has(o)) scene.add(o); }); oldRO.forEach(o => { if (!newRO.has(o)) scene.remove(o); }); } } else { repr.renderObjects.forEach(o => scene.add(o)); } reprRenderObjects.set(repr, newRO); scene.update(repr.renderObjects, false); forceDrawAfterAllCommited = true; if (debug_1.isDebugMode) consoleStats(); } function remove(repr) { unregisterAutoUpdate(repr); const renderObjects = reprRenderObjects.get(repr); if (renderObjects) { renderObjects.forEach(o => scene.remove(o)); reprRenderObjects.delete(repr); forceDrawAfterAllCommited = true; if (debug_1.isDebugMode) consoleStats(); } } function registerAutoUpdate(repr) { if (reprUpdatedSubscriptions.has(repr)) return; reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => { if (!repr.state.syncManually) add(repr); })); } function unregisterAutoUpdate(repr) { const updatedSubscription = reprUpdatedSubscriptions.get(repr); if (updatedSubscription) { updatedSubscription.unsubscribe(); reprUpdatedSubscriptions.delete(repr); } } function getProps() { const radius = scene.boundingSphere.radius > 0 ? 100 - Math.round((camera.transition.target.radius / getSceneRadius()) * 100) : 0; return { camera: { mode: camera.state.mode, helper: { ...helper.camera.props }, stereo: { ...p.camera.stereo }, fov: Math.round((0, misc_1.radToDeg)(camera.state.fov)), manualReset: !!p.camera.manualReset }, cameraFog: camera.state.fog > 0 ? { name: 'on', params: { intensity: camera.state.fog } } : { name: 'off', params: {} }, cameraClipping: { far: camera.state.clipFar, radius, minNear: camera.state.minNear, }, cameraResetDurationMs: p.cameraResetDurationMs, sceneRadiusFactor: p.sceneRadiusFactor, transparentBackground: p.transparentBackground, checkeredTransparentBackground: p.checkeredTransparentBackground, dpoitIterations: p.dpoitIterations, pickPadding: p.pickPadding, userInteractionReleaseMs: p.userInteractionReleaseMs, viewport: p.viewport, postprocessing: { ...p.postprocessing }, marking: { ...p.marking }, multiSample: { ...p.multiSample }, illumination: { ...p.illumination }, hiZ: { ...hiZ.props }, renderer: { ...renderer.props }, trackball: { ...controls.props }, interaction: { ...interactionHelper.props }, debug: { ...helper.debug.props }, handle: { ...helper.handle.props }, pointer: { ...helper.pointer.props }, xr: { ...xrManager.props }, }; } const contextLostSub = contextLost === null || contextLost === void 0 ? void 0 : contextLost.subscribe(() => { isContextLost = true; pickHelper.dirty = true; }); const contextRestoredSub = contextRestored.subscribe(() => { pickHelper.reset(); rayHelper.reset(); hiZ.reset(); scene.forEach(r => { var _a; if ((_a = r.values.meta) === null || _a === void 0 ? void 0 : _a.ref.value.reset) { r.values.meta.ref.value.reset(); r.update(); } }); isContextLost = false; draw({ force: true }); // Unclear why, but in Chrome with wboit enabled the first `draw` only clears // the drawingBuffer. Note that in Firefox the drawingBuffer is preserved after // context loss so it is unclear if it behaves the same. draw({ force: true }); }); const resized = new rxjs_1.BehaviorSubject(0); function handleResize(draw = true) { passes.updateSize(); updateViewport(); syncViewport(); if (draw) requestDraw(); resized.next(+new Date()); } (0, debug_1.addConsoleStatsProvider)(consoleStats); const ctxChangedSub = (_b = ctx.changed) === null || _b === void 0 ? void 0 : _b.subscribe(() => { scene.setTransparency(passes.draw.transparency); requestDraw(); }); // Monitor user interactions let isDragging = false; let isActivelyInteracting = false; const interactionSubs = [ input.drag.subscribe(() => { isDragging = true; }), input.interactionEnd.subscribe(() => { isDragging = false; }), (0, rxjs_1.merge)(input.drag, input.pinch, input.wheel, input.interactionEnd).subscribe(() => { interactionEvent.next(); }), interactionEvent.subscribe(() => { isActivelyInteracting = true; }), interactionEvent.pipe((0, rxjs_1.debounceTime)(p.userInteractionReleaseMs)).subscribe(() => { isActivelyInteracting = isDragging; if (!isDragging && passes.illumination.supported && p.illumination.enabled) { requestDraw(); } }), ]; // if (debug_1.isDebugMode && canvas) { let occlusionLoci = undefined; const printOcclusion = (loci) => { const s = loci && loci_1.Loci.getBoundingSphere(loci_1.Loci.normalize(loci, 'residue')); hiZ.debugOcclusion(s); }; input.click.subscribe(e => { if (!e.modifiers.control || e.button !== 2) return; const p = identify(linear_algebra_1.Vec2.create(e.x, e.y)); if (!p) { occlusionLoci = undefined; printOcclusion(occlusionLoci); return; } const l = getLoci(p.id); occlusionLoci = l.loci; printOcclusion(occlusionLoci); }); didDraw.subscribe(() => { setTimeout(() => { printOcclusion(occlusionLoci); }, 100); }); } // return { webgl, add, remove, commit, update: (repr, keepSphere) => { if (repr) { if (!reprRenderObjects.has(repr)) return; scene.update(repr.renderObjects, !!keepSphere); } else { scene.update(void 0, !!keepSphere); } forceDrawAfterAllCommited = true; }, clear: () => { reprUpdatedSubscriptions.forEach(v => v.unsubscribe()); reprUpdatedSubscriptions.clear(); reprRenderObjects.clear(); scene.clear(); helper.debug.clear(); requestDraw(); reprCount.next(reprRenderObjects.size); }, syncVisibility: () => { if (camera.state.radiusMax === 0) { cameraResetRequested = true; nextCameraResetDuration = 0; } if (scene.syncVisibility()) { if (helper.debug.isEnabled) helper.debug.update(); } requestDraw(); }, requestDraw, tick, animate, resetTime, pause, resume, requestAnimationFrame: _requestAnimationFrame, cancelAnimationFrame: _cancelAnimationFrame, identify, asyncIdentify, mark, getLoci, handleResize, requestResize: () => { resizeRequested = true; }, requestCameraReset: options => { nextCameraResetDuration = options === null || options === void 0 ? void 0 : options.durationMs; nextCameraResetSnapshot = options === null || options === void 0 ? void 0 : options.snapshot; cameraResetRequested = true;