canvas3d.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. /**
  2. * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. */
  7. import { BehaviorSubject, Subscription } from 'rxjs';
  8. import { now } from '../mol-util/now';
  9. import { Vec3, Vec2 } from '../mol-math/linear-algebra';
  10. import { InputObserver, ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
  11. import { Renderer, RendererStats, RendererParams } from '../mol-gl/renderer';
  12. import { GraphicsRenderObject } from '../mol-gl/render-object';
  13. import { TrackballControls, TrackballControlsParams } from './controls/trackball';
  14. import { Viewport } from './camera/util';
  15. import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
  16. import { Representation } from '../mol-repr/representation';
  17. import { Scene } from '../mol-gl/scene';
  18. import { PickingId } from '../mol-geo/geometry/picking';
  19. import { MarkerAction } from '../mol-util/marker-action';
  20. import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
  21. import { Camera } from './camera';
  22. import { ParamDefinition as PD } from '../mol-util/param-definition';
  23. import { DebugHelperParams } from './helper/bounding-sphere-helper';
  24. import { SetUtils } from '../mol-util/set';
  25. import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events';
  26. import { PostprocessingParams } from './passes/postprocessing';
  27. import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
  28. import { PickData } from './passes/pick';
  29. import { PickHelper } from './passes/pick';
  30. import { ImagePass, ImageProps } from './passes/image';
  31. import { Sphere3D } from '../mol-math/geometry';
  32. import { isDebugMode } from '../mol-util/debug';
  33. import { CameraHelperParams } from './helper/camera-helper';
  34. import { produce } from 'immer';
  35. import { HandleHelperParams } from './helper/handle-helper';
  36. import { StereoCamera, StereoCameraParams } from './camera/stereo';
  37. import { Helper } from './helper/helper';
  38. import { Passes } from './passes/passes';
  39. import { shallowEqual } from '../mol-util';
  40. import { MarkingParams } from './passes/marking';
  41. import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit } from '../mol-gl/webgl/render-item';
  42. export const Canvas3DParams = {
  43. camera: PD.Group({
  44. mode: PD.Select('perspective', PD.arrayToOptions(['perspective', 'orthographic'] as const), { label: 'Camera' }),
  45. helper: PD.Group(CameraHelperParams, { isFlat: true }),
  46. stereo: PD.MappedStatic('off', {
  47. on: PD.Group(StereoCameraParams),
  48. off: PD.Group({})
  49. }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
  50. manualReset: PD.Boolean(false, { isHidden: true }),
  51. }, { pivot: 'mode' }),
  52. cameraFog: PD.MappedStatic('on', {
  53. on: PD.Group({
  54. intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
  55. }),
  56. off: PD.Group({})
  57. }, { cycle: true, description: 'Show fog in the distance' }),
  58. cameraClipping: PD.Group({
  59. radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
  60. far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
  61. }, { pivot: 'radius' }),
  62. viewport: PD.MappedStatic('canvas', {
  63. canvas: PD.Group({}),
  64. 'static-frame': PD.Group({
  65. x: PD.Numeric(0),
  66. y: PD.Numeric(0),
  67. width: PD.Numeric(128),
  68. height: PD.Numeric(128)
  69. }),
  70. 'relative-frame': PD.Group({
  71. x: PD.Numeric(0.33, { min: 0, max: 1, step: 0.01 }),
  72. y: PD.Numeric(0.33, { min: 0, max: 1, step: 0.01 }),
  73. width: PD.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 }),
  74. height: PD.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 })
  75. })
  76. }),
  77. cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
  78. transparentBackground: PD.Boolean(false),
  79. multiSample: PD.Group(MultiSampleParams),
  80. postprocessing: PD.Group(PostprocessingParams),
  81. marking: PD.Group(MarkingParams),
  82. renderer: PD.Group(RendererParams),
  83. trackball: PD.Group(TrackballControlsParams),
  84. interaction: PD.Group(Canvas3dInteractionHelperParams),
  85. debug: PD.Group(DebugHelperParams),
  86. handle: PD.Group(HandleHelperParams),
  87. };
  88. export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
  89. export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
  90. export type PartialCanvas3DProps = {
  91. [K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]>
  92. }
  93. export { Canvas3DContext };
  94. /** Can be used to create multiple Canvas3D objects */
  95. interface Canvas3DContext {
  96. readonly canvas: HTMLCanvasElement
  97. readonly webgl: WebGLContext
  98. readonly input: InputObserver
  99. readonly passes: Passes
  100. readonly attribs: Readonly<Canvas3DContext.Attribs>
  101. readonly contextLost: BehaviorSubject<now.Timestamp>
  102. readonly contextRestored: BehaviorSubject<now.Timestamp>
  103. dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
  104. }
  105. namespace Canvas3DContext {
  106. export const DefaultAttribs = {
  107. /** true by default to avoid issues with Safari (Jan 2021) */
  108. antialias: true,
  109. /** true to support multiple Canvas3D objects with a single context */
  110. preserveDrawingBuffer: true,
  111. pixelScale: 1,
  112. pickScale: 0.25,
  113. /** extra pixels to around target to check in case target is empty */
  114. pickPadding: 1,
  115. enableWboit: true,
  116. preferWebGl1: false
  117. };
  118. export type Attribs = typeof DefaultAttribs
  119. export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
  120. const a = { ...DefaultAttribs, ...attribs };
  121. const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
  122. const gl = getGLContext(canvas, {
  123. antialias,
  124. preserveDrawingBuffer,
  125. alpha: true, // the renderer requires an alpha channel
  126. depth: true, // the renderer requires a depth buffer
  127. premultipliedAlpha: true, // the renderer outputs PMA
  128. preferWebGl1
  129. });
  130. if (gl === null) throw new Error('Could not create a WebGL rendering context');
  131. const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
  132. const webgl = createContext(gl, { pixelScale });
  133. const passes = new Passes(webgl, attribs);
  134. if (isDebugMode) {
  135. const loseContextExt = gl.getExtension('WEBGL_lose_context');
  136. if (loseContextExt) {
  137. // Hold down shift+ctrl+alt and press any mouse button to call `loseContext`.
  138. // After 1 second `restoreContext` will be called.
  139. canvas.addEventListener('mousedown', e => {
  140. if (webgl.isContextLost) return;
  141. if (!e.shiftKey || !e.ctrlKey || !e.altKey) return;
  142. if (isDebugMode) console.log('lose context');
  143. loseContextExt.loseContext();
  144. setTimeout(() => {
  145. if (!webgl.isContextLost) return;
  146. if (isDebugMode) console.log('restore context');
  147. loseContextExt.restoreContext();
  148. }, 1000);
  149. }, false);
  150. }
  151. }
  152. // https://www.khronos.org/webgl/wiki/HandlingContextLost
  153. const contextLost = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  154. const handleWebglContextLost = (e: Event) => {
  155. webgl.setContextLost();
  156. e.preventDefault();
  157. if (isDebugMode) console.log('context lost');
  158. contextLost.next(now());
  159. };
  160. const handlewWebglContextRestored = () => {
  161. if (!webgl.isContextLost) return;
  162. webgl.handleContextRestored(() => {
  163. passes.draw.reset();
  164. });
  165. if (isDebugMode) console.log('context restored');
  166. };
  167. canvas.addEventListener('webglcontextlost', handleWebglContextLost, false);
  168. canvas.addEventListener('webglcontextrestored', handlewWebglContextRestored, false);
  169. return {
  170. canvas,
  171. webgl,
  172. input,
  173. passes,
  174. attribs: a,
  175. contextLost,
  176. contextRestored: webgl.contextRestored,
  177. dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
  178. input.dispose();
  179. canvas.removeEventListener('webglcontextlost', handleWebglContextLost, false);
  180. canvas.removeEventListener('webglcontextrestored', handlewWebglContextRestored, false);
  181. webgl.destroy(options);
  182. }
  183. };
  184. }
  185. }
  186. export { Canvas3D };
  187. interface Canvas3D {
  188. readonly webgl: WebGLContext,
  189. add(repr: Representation.Any): void
  190. remove(repr: Representation.Any): void
  191. /**
  192. * This function must be called if animate() is not set up so that add/remove actions take place.
  193. */
  194. commit(isSynchronous?: boolean): void
  195. /**
  196. * Function for external "animation" control
  197. * Calls commit.
  198. */
  199. tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
  200. update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
  201. clear(): void
  202. syncVisibility(): void
  203. requestDraw(force?: boolean): void
  204. /** Reset the timers, used by "animate" */
  205. resetTime(t: number): void
  206. animate(): void
  207. /**
  208. * Pause animation loop and optionally any rendering
  209. * @param noDraw pause any rendering (drawPaused = true)
  210. */
  211. pause(noDraw?: boolean): void
  212. /** Sets drawPaused = false without starting the built in animation loop */
  213. resume(): void
  214. identify(x: number, y: number): PickData | undefined
  215. mark(loci: Representation.Loci, action: MarkerAction, noDraw?: boolean): void
  216. getLoci(pickingId: PickingId | undefined): Representation.Loci
  217. notifyDidDraw: boolean,
  218. readonly didDraw: BehaviorSubject<now.Timestamp>
  219. readonly commited: BehaviorSubject<now.Timestamp>
  220. readonly reprCount: BehaviorSubject<number>
  221. readonly resized: BehaviorSubject<any>
  222. handleResize(): void
  223. /** performs handleResize on the next animation frame */
  224. requestResize(): void
  225. /** Focuses camera on scene's bounding sphere, centered and zoomed. */
  226. requestCameraReset(options?: { durationMs?: number, snapshot?: Camera.SnapshotProvider }): void
  227. readonly camera: Camera
  228. readonly boundingSphere: Readonly<Sphere3D>
  229. readonly boundingSphereVisible: Readonly<Sphere3D>
  230. setProps(props: PartialCanvas3DProps | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void), doNotRequestDraw?: boolean /* = false */): void
  231. getImagePass(props: Partial<ImageProps>): ImagePass
  232. getRenderObjects(): GraphicsRenderObject[]
  233. /** Returns a copy of the current Canvas3D instance props */
  234. readonly props: Readonly<Canvas3DProps>
  235. readonly input: InputObserver
  236. readonly stats: RendererStats
  237. readonly interaction: Canvas3dInteractionHelper['events']
  238. dispose(): void
  239. }
  240. const requestAnimationFrame = typeof window !== 'undefined'
  241. ? window.requestAnimationFrame
  242. : (f: (time: number) => void) => setImmediate(() => f(Date.now())) as unknown as number;
  243. const cancelAnimationFrame = typeof window !== 'undefined'
  244. ? window.cancelAnimationFrame
  245. : (handle: number) => clearImmediate(handle as unknown as NodeJS.Immediate);
  246. namespace Canvas3D {
  247. export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
  248. export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
  249. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
  250. export function create({ webgl, input, passes, attribs }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
  251. const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
  252. const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
  253. const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
  254. const reprCount = new BehaviorSubject(0);
  255. let startTime = now();
  256. const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  257. const commited = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  258. const { gl, contextRestored } = webgl;
  259. let x = 0;
  260. let y = 0;
  261. let width = 128;
  262. let height = 128;
  263. updateViewport();
  264. const scene = Scene.create(webgl, passes.draw.wboitEnabled ? GraphicsRenderVariantsWboit : GraphicsRenderVariantsBlended);
  265. const camera = new Camera({
  266. position: Vec3.create(0, 0, 100),
  267. mode: p.camera.mode,
  268. fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
  269. clipFar: p.cameraClipping.far
  270. }, { x, y, width, height }, { pixelScale: attribs.pixelScale });
  271. const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
  272. const controls = TrackballControls.create(input, camera, p.trackball);
  273. const renderer = Renderer.create(webgl, p.renderer);
  274. const helper = new Helper(webgl, scene, p);
  275. const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, attribs.pickPadding);
  276. const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
  277. const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
  278. let cameraResetRequested = false;
  279. let nextCameraResetDuration: number | undefined = void 0;
  280. let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
  281. let resizeRequested = false;
  282. let notifyDidDraw = true;
  283. function getLoci(pickingId: PickingId | undefined) {
  284. let loci: Loci = EmptyLoci;
  285. let repr: Representation.Any = Representation.Empty;
  286. if (pickingId) {
  287. const cameraHelperLoci = helper.camera.getLoci(pickingId);
  288. if (cameraHelperLoci !== EmptyLoci) return { loci: cameraHelperLoci, repr };
  289. loci = helper.handle.getLoci(pickingId);
  290. reprRenderObjects.forEach((_, _repr) => {
  291. const _loci = _repr.getLoci(pickingId);
  292. if (!isEmptyLoci(_loci)) {
  293. if (!isEmptyLoci(loci)) {
  294. console.warn('found another loci, this should not happen');
  295. }
  296. loci = _loci;
  297. repr = _repr;
  298. }
  299. });
  300. }
  301. return { loci, repr };
  302. }
  303. function mark(reprLoci: Representation.Loci, action: MarkerAction, noDraw = false) {
  304. const { repr, loci } = reprLoci;
  305. let changed = false;
  306. if (repr) {
  307. changed = repr.mark(loci, action);
  308. } else {
  309. changed = helper.handle.mark(loci, action);
  310. changed = helper.camera.mark(loci, action) || changed;
  311. reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
  312. }
  313. if (changed) {
  314. if (noDraw) {
  315. // Even with `noDraw` make sure changes will be rendered.
  316. // Note that with this calling mark (with or without `noDraw`) multiple times
  317. // during a JS event loop iteration will only result in a single render call.
  318. forceNextRender = true;
  319. } else {
  320. scene.update(void 0, true);
  321. helper.handle.scene.update(void 0, true);
  322. helper.camera.scene.update(void 0, true);
  323. const prevPickDirty = pickHelper.dirty;
  324. draw(true, true);
  325. pickHelper.dirty = prevPickDirty; // marking does not change picking buffers
  326. }
  327. }
  328. }
  329. function render(force: boolean, allowMulti: boolean) {
  330. if (webgl.isContextLost) return false;
  331. let resized = false;
  332. if (resizeRequested) {
  333. handleResize(false);
  334. resizeRequested = false;
  335. resized = true;
  336. }
  337. if (x > gl.drawingBufferWidth || x + width < 0 ||
  338. y > gl.drawingBufferHeight || y + height < 0
  339. ) return false;
  340. let didRender = false;
  341. controls.update(currentTime);
  342. const cameraChanged = camera.update();
  343. const shouldRender = force || cameraChanged || resized || forceNextRender;
  344. forceNextRender = false;
  345. const multiSampleChanged = multiSampleHelper.update(shouldRender, p.multiSample);
  346. if (shouldRender || multiSampleChanged) {
  347. let cam: Camera | StereoCamera = camera;
  348. if (p.camera.stereo.name === 'on') {
  349. stereoCamera.update();
  350. cam = stereoCamera;
  351. }
  352. if (MultiSamplePass.isEnabled(p.multiSample)) {
  353. if (!cameraChanged && allowMulti && !controls.props.spin) {
  354. while (!multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p));
  355. } else {
  356. multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
  357. }
  358. } else {
  359. passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing, p.marking);
  360. }
  361. pickHelper.dirty = true;
  362. didRender = true;
  363. }
  364. return didRender;
  365. }
  366. let forceNextRender = false;
  367. let forceDrawAfterAllCommited = false;
  368. let currentTime = 0;
  369. let drawPaused = false;
  370. function draw(force?: boolean, allowMulti?: boolean) {
  371. if (drawPaused) return;
  372. if (render(!!force, !!allowMulti) && notifyDidDraw) {
  373. didDraw.next(now() - startTime as now.Timestamp);
  374. }
  375. }
  376. function requestDraw(force?: boolean) {
  377. forceNextRender = forceNextRender || !!force;
  378. }
  379. let animationFrameHandle = 0;
  380. function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }) {
  381. currentTime = t;
  382. commit(options?.isSynchronous);
  383. camera.transition.tick(currentTime);
  384. if (options?.manualDraw) {
  385. return;
  386. }
  387. draw(false);
  388. if (!camera.transition.inTransition && !webgl.isContextLost) {
  389. interactionHelper.tick(currentTime);
  390. }
  391. }
  392. function _animate() {
  393. tick(now());
  394. animationFrameHandle = requestAnimationFrame(_animate);
  395. }
  396. function resetTime(t: now.Timestamp) {
  397. startTime = t;
  398. controls.start(t);
  399. }
  400. function animate() {
  401. drawPaused = false;
  402. controls.start(now());
  403. if (animationFrameHandle === 0) _animate();
  404. }
  405. function pause(noDraw = false) {
  406. drawPaused = noDraw;
  407. cancelAnimationFrame(animationFrameHandle);
  408. animationFrameHandle = 0;
  409. }
  410. function identify(x: number, y: number): PickData | undefined {
  411. const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
  412. return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
  413. }
  414. function commit(isSynchronous: boolean = false) {
  415. const allCommited = commitScene(isSynchronous);
  416. // Only reset the camera after the full scene has been commited.
  417. if (allCommited) {
  418. resolveCameraReset();
  419. if (forceDrawAfterAllCommited) {
  420. if (helper.debug.isEnabled) helper.debug.update();
  421. draw(true);
  422. forceDrawAfterAllCommited = false;
  423. }
  424. commited.next(now());
  425. }
  426. }
  427. function resolveCameraReset() {
  428. if (!cameraResetRequested) return;
  429. const boundingSphere = scene.boundingSphereVisible;
  430. const { center, radius } = boundingSphere;
  431. const autoAdjustControls = controls.props.autoAdjustMinMaxDistance;
  432. if (autoAdjustControls.name === 'on') {
  433. const minDistance = autoAdjustControls.params.minDistanceFactor * radius + autoAdjustControls.params.minDistancePadding;
  434. const maxDistance = Math.max(autoAdjustControls.params.maxDistanceFactor * radius, autoAdjustControls.params.maxDistanceMin);
  435. controls.setProps({ minDistance, maxDistance });
  436. }
  437. if (radius > 0) {
  438. const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration;
  439. const focus = camera.getFocus(center, radius);
  440. const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
  441. const snapshot = next ? { ...focus, ...next } : focus;
  442. camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration);
  443. }
  444. nextCameraResetDuration = void 0;
  445. nextCameraResetSnapshot = void 0;
  446. cameraResetRequested = false;
  447. }
  448. const oldBoundingSphereVisible = Sphere3D();
  449. const cameraSphere = Sphere3D();
  450. function shouldResetCamera() {
  451. if (camera.state.radiusMax === 0) return true;
  452. if (camera.transition.inTransition || nextCameraResetSnapshot) return false;
  453. let cameraSphereOverlapsNone = true, isEmpty = true;
  454. Sphere3D.set(cameraSphere, camera.state.target, camera.state.radius);
  455. // check if any renderable has moved outside of the old bounding sphere
  456. // and if no renderable is overlapping with the camera sphere
  457. for (const r of scene.renderables) {
  458. if (!r.state.visible) continue;
  459. const b = r.values.boundingSphere.ref.value;
  460. if (!b.radius) continue;
  461. isEmpty = false;
  462. const cameraDist = Vec3.distance(cameraSphere.center, b.center);
  463. if ((cameraDist > cameraSphere.radius || cameraDist > b.radius || b.radius > camera.state.radiusMax) && !Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
  464. if (Sphere3D.overlaps(cameraSphere, b)) cameraSphereOverlapsNone = false;
  465. }
  466. return cameraSphereOverlapsNone || (!isEmpty && cameraSphere.radius <= 0.1);
  467. }
  468. const sceneCommitTimeoutMs = 250;
  469. function commitScene(isSynchronous: boolean) {
  470. if (!scene.needsCommit) return true;
  471. // snapshot the current bounding sphere of visible objects
  472. Sphere3D.copy(oldBoundingSphereVisible, scene.boundingSphereVisible);
  473. if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
  474. if (helper.debug.isEnabled) helper.debug.update();
  475. if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
  476. cameraResetRequested = true;
  477. }
  478. if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
  479. camera.setState({ radiusMax: scene.boundingSphere.radius }, 0);
  480. reprCount.next(reprRenderObjects.size);
  481. if (isDebugMode) consoleStats();
  482. return true;
  483. }
  484. function consoleStats() {
  485. console.table(scene.renderables.map(r => ({
  486. drawCount: r.values.drawCount.ref.value,
  487. instanceCount: r.values.instanceCount.ref.value,
  488. materialId: r.materialId,
  489. renderItemId: r.id,
  490. })));
  491. console.log(webgl.stats);
  492. const { texture, attribute, elements } = webgl.resources.getByteCounts();
  493. console.log({
  494. texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
  495. attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
  496. elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
  497. });
  498. }
  499. function add(repr: Representation.Any) {
  500. registerAutoUpdate(repr);
  501. const oldRO = reprRenderObjects.get(repr);
  502. const newRO = new Set<GraphicsRenderObject>();
  503. repr.renderObjects.forEach(o => newRO.add(o));
  504. if (oldRO) {
  505. if (!SetUtils.areEqual(newRO, oldRO)) {
  506. newRO.forEach(o => { if (!oldRO.has(o)) scene.add(o); });
  507. oldRO.forEach(o => { if (!newRO.has(o)) scene.remove(o); });
  508. }
  509. } else {
  510. repr.renderObjects.forEach(o => scene.add(o));
  511. }
  512. reprRenderObjects.set(repr, newRO);
  513. scene.update(repr.renderObjects, false);
  514. forceDrawAfterAllCommited = true;
  515. if (isDebugMode) consoleStats();
  516. }
  517. function remove(repr: Representation.Any) {
  518. unregisterAutoUpdate(repr);
  519. const renderObjects = reprRenderObjects.get(repr);
  520. if (renderObjects) {
  521. renderObjects.forEach(o => scene.remove(o));
  522. reprRenderObjects.delete(repr);
  523. forceDrawAfterAllCommited = true;
  524. if (isDebugMode) consoleStats();
  525. }
  526. }
  527. function registerAutoUpdate(repr: Representation.Any) {
  528. if (reprUpdatedSubscriptions.has(repr)) return;
  529. reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
  530. if (!repr.state.syncManually) add(repr);
  531. }));
  532. }
  533. function unregisterAutoUpdate(repr: Representation.Any) {
  534. const updatedSubscription = reprUpdatedSubscriptions.get(repr);
  535. if (updatedSubscription) {
  536. updatedSubscription.unsubscribe();
  537. reprUpdatedSubscriptions.delete(repr);
  538. }
  539. }
  540. function getProps(): Canvas3DProps {
  541. const radius = scene.boundingSphere.radius > 0
  542. ? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100)
  543. : 0;
  544. return {
  545. camera: {
  546. mode: camera.state.mode,
  547. helper: { ...helper.camera.props },
  548. stereo: { ...p.camera.stereo },
  549. manualReset: !!p.camera.manualReset
  550. },
  551. cameraFog: camera.state.fog > 0
  552. ? { name: 'on' as const, params: { intensity: camera.state.fog } }
  553. : { name: 'off' as const, params: {} },
  554. cameraClipping: { far: camera.state.clipFar, radius },
  555. cameraResetDurationMs: p.cameraResetDurationMs,
  556. transparentBackground: p.transparentBackground,
  557. viewport: p.viewport,
  558. postprocessing: { ...p.postprocessing },
  559. marking: { ...p.marking },
  560. multiSample: { ...p.multiSample },
  561. renderer: { ...renderer.props },
  562. trackball: { ...controls.props },
  563. interaction: { ...interactionHelper.props },
  564. debug: { ...helper.debug.props },
  565. handle: { ...helper.handle.props },
  566. };
  567. }
  568. const contextRestoredSub = contextRestored.subscribe(() => {
  569. pickHelper.dirty = true;
  570. draw(true);
  571. // Unclear why, but in Chrome with wboit enabled the first `draw` only clears
  572. // the drawingBuffer. Note that in Firefox the drawingBuffer is preserved after
  573. // context loss so it is unclear if it behaves the same.
  574. draw(true);
  575. });
  576. const resized = new BehaviorSubject<any>(0);
  577. function handleResize(draw = true) {
  578. passes.updateSize();
  579. updateViewport();
  580. syncViewport();
  581. if (draw) requestDraw(true);
  582. resized.next(+new Date());
  583. }
  584. return {
  585. webgl,
  586. add,
  587. remove,
  588. commit,
  589. update: (repr, keepSphere) => {
  590. if (repr) {
  591. if (!reprRenderObjects.has(repr)) return;
  592. scene.update(repr.renderObjects, !!keepSphere);
  593. } else {
  594. scene.update(void 0, !!keepSphere);
  595. }
  596. forceDrawAfterAllCommited = true;
  597. },
  598. clear: () => {
  599. reprUpdatedSubscriptions.forEach(v => v.unsubscribe());
  600. reprUpdatedSubscriptions.clear();
  601. reprRenderObjects.clear();
  602. scene.clear();
  603. helper.debug.clear();
  604. requestDraw(true);
  605. reprCount.next(reprRenderObjects.size);
  606. },
  607. syncVisibility: () => {
  608. if (camera.state.radiusMax === 0) {
  609. cameraResetRequested = true;
  610. nextCameraResetDuration = 0;
  611. }
  612. if (scene.syncVisibility()) {
  613. if (helper.debug.isEnabled) helper.debug.update();
  614. }
  615. requestDraw(true);
  616. },
  617. requestDraw,
  618. tick,
  619. animate,
  620. resetTime,
  621. pause,
  622. resume: () => { drawPaused = false; },
  623. identify,
  624. mark,
  625. getLoci,
  626. handleResize,
  627. requestResize: () => {
  628. resizeRequested = true;
  629. },
  630. requestCameraReset: options => {
  631. nextCameraResetDuration = options?.durationMs;
  632. nextCameraResetSnapshot = options?.snapshot;
  633. cameraResetRequested = true;
  634. },
  635. camera,
  636. boundingSphere: scene.boundingSphere,
  637. boundingSphereVisible: scene.boundingSphereVisible,
  638. get notifyDidDraw() { return notifyDidDraw; },
  639. set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
  640. didDraw,
  641. commited,
  642. reprCount,
  643. resized,
  644. setProps: (properties, doNotRequestDraw = false) => {
  645. const props: PartialCanvas3DProps = typeof properties === 'function'
  646. ? produce(getProps(), properties as any)
  647. : properties;
  648. const cameraState: Partial<Camera.Snapshot> = Object.create(null);
  649. if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
  650. cameraState.mode = props.camera.mode;
  651. }
  652. if (props.cameraFog !== undefined && props.cameraFog.params) {
  653. const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
  654. if (newFog !== camera.state.fog) cameraState.fog = newFog;
  655. }
  656. if (props.cameraClipping !== undefined) {
  657. if (props.cameraClipping.far !== undefined && props.cameraClipping.far !== camera.state.clipFar) {
  658. cameraState.clipFar = props.cameraClipping.far;
  659. }
  660. if (props.cameraClipping.radius !== undefined) {
  661. const radius = (scene.boundingSphere.radius / 100) * (100 - props.cameraClipping.radius);
  662. if (radius > 0 && radius !== cameraState.radius) {
  663. // if radius = 0, NaNs happen
  664. cameraState.radius = Math.max(radius, 0.01);
  665. }
  666. }
  667. }
  668. if (Object.keys(cameraState).length > 0) camera.setState(cameraState);
  669. if (props.camera?.helper) helper.camera.setProps(props.camera.helper);
  670. if (props.camera?.manualReset !== undefined) p.camera.manualReset = props.camera.manualReset;
  671. if (props.camera?.stereo !== undefined) Object.assign(p.camera.stereo, props.camera.stereo);
  672. if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
  673. if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
  674. if (props.viewport !== undefined) {
  675. const doNotUpdate = p.viewport === props.viewport ||
  676. (p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
  677. if (!doNotUpdate) {
  678. p.viewport = props.viewport;
  679. updateViewport();
  680. syncViewport();
  681. }
  682. }
  683. if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
  684. if (props.marking) Object.assign(p.marking, props.marking);
  685. if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
  686. if (props.renderer) renderer.setProps(props.renderer);
  687. if (props.trackball) controls.setProps(props.trackball);
  688. if (props.interaction) interactionHelper.setProps(props.interaction);
  689. if (props.debug) helper.debug.setProps(props.debug);
  690. if (props.handle) helper.handle.setProps(props.handle);
  691. if (cameraState.mode === 'orthographic') {
  692. p.camera.stereo.name = 'off';
  693. }
  694. if (!doNotRequestDraw) {
  695. requestDraw(true);
  696. }
  697. },
  698. getImagePass: (props: Partial<ImageProps> = {}) => {
  699. return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
  700. },
  701. getRenderObjects(): GraphicsRenderObject[] {
  702. const renderObjects: GraphicsRenderObject[] = [];
  703. scene.forEach((_, ro) => renderObjects.push(ro));
  704. return renderObjects;
  705. },
  706. get props() {
  707. return getProps();
  708. },
  709. get input() {
  710. return input;
  711. },
  712. get stats() {
  713. return renderer.stats;
  714. },
  715. get interaction() {
  716. return interactionHelper.events;
  717. },
  718. dispose: () => {
  719. contextRestoredSub.unsubscribe();
  720. scene.clear();
  721. helper.debug.clear();
  722. controls.dispose();
  723. renderer.dispose();
  724. interactionHelper.dispose();
  725. }
  726. };
  727. function updateViewport() {
  728. const oldX = x, oldY = y, oldWidth = width, oldHeight = height;
  729. if (p.viewport.name === 'canvas') {
  730. x = 0;
  731. y = 0;
  732. width = gl.drawingBufferWidth;
  733. height = gl.drawingBufferHeight;
  734. } else if (p.viewport.name === 'static-frame') {
  735. x = p.viewport.params.x * webgl.pixelRatio;
  736. height = p.viewport.params.height * webgl.pixelRatio;
  737. y = gl.drawingBufferHeight - height - p.viewport.params.y * webgl.pixelRatio;
  738. width = p.viewport.params.width * webgl.pixelRatio;
  739. } else if (p.viewport.name === 'relative-frame') {
  740. x = Math.round(p.viewport.params.x * gl.drawingBufferWidth);
  741. height = Math.round(p.viewport.params.height * gl.drawingBufferHeight);
  742. y = Math.round(gl.drawingBufferHeight - height - p.viewport.params.y * gl.drawingBufferHeight);
  743. width = Math.round(p.viewport.params.width * gl.drawingBufferWidth);
  744. }
  745. if (oldX !== x || oldY !== y || oldWidth !== width || oldHeight !== height) {
  746. forceNextRender = true;
  747. }
  748. }
  749. function syncViewport() {
  750. pickHelper.setViewport(x, y, width, height);
  751. renderer.setViewport(x, y, width, height);
  752. Viewport.set(camera.viewport, x, y, width, height);
  753. Viewport.set(controls.viewport, x, y, width, height);
  754. }
  755. }
  756. }