canvas3d.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  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 } 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. export const Canvas3DParams = {
  41. camera: PD.Group({
  42. mode: PD.Select('perspective', PD.arrayToOptions(['perspective', 'orthographic'] as const), { label: 'Camera' }),
  43. helper: PD.Group(CameraHelperParams, { isFlat: true }),
  44. stereo: PD.MappedStatic('off', {
  45. on: PD.Group(StereoCameraParams),
  46. off: PD.Group({})
  47. }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
  48. manualReset: PD.Boolean(false, { isHidden: true }),
  49. }, { pivot: 'mode' }),
  50. cameraFog: PD.MappedStatic('on', {
  51. on: PD.Group({
  52. intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
  53. }),
  54. off: PD.Group({})
  55. }, { cycle: true, description: 'Show fog in the distance' }),
  56. cameraClipping: PD.Group({
  57. radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
  58. far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
  59. }, { pivot: 'radius' }),
  60. viewport: PD.MappedStatic('canvas', {
  61. canvas: PD.Group({}),
  62. custom: PD.Group({
  63. x: PD.Numeric(0),
  64. y: PD.Numeric(0),
  65. width: PD.Numeric(128),
  66. height: PD.Numeric(128)
  67. })
  68. }),
  69. cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
  70. transparentBackground: PD.Boolean(false),
  71. multiSample: PD.Group(MultiSampleParams),
  72. postprocessing: PD.Group(PostprocessingParams),
  73. renderer: PD.Group(RendererParams),
  74. trackball: PD.Group(TrackballControlsParams),
  75. debug: PD.Group(DebugHelperParams),
  76. handle: PD.Group(HandleHelperParams),
  77. };
  78. export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
  79. export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
  80. export type PartialCanvas3DProps = {
  81. [K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]>
  82. }
  83. export { Canvas3D };
  84. interface Canvas3D {
  85. readonly webgl: WebGLContext,
  86. add(repr: Representation.Any): void
  87. remove(repr: Representation.Any): void
  88. /**
  89. * This function must be called if animate() is not set up so that add/remove actions take place.
  90. */
  91. commit(isSynchronous?: boolean): void
  92. /**
  93. * Funcion for external "animation" control
  94. * Calls commit.
  95. */
  96. tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
  97. update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
  98. clear(): void
  99. syncVisibility(): void
  100. requestDraw(force?: boolean): void
  101. /** Reset the timers, used by "animate" */
  102. resetTime(t: number): void
  103. animate(): void
  104. pause(): void
  105. identify(x: number, y: number): PickData | undefined
  106. mark(loci: Representation.Loci, action: MarkerAction): void
  107. getLoci(pickingId: PickingId | undefined): Representation.Loci
  108. notifyDidDraw: boolean,
  109. readonly didDraw: BehaviorSubject<now.Timestamp>
  110. readonly commited: BehaviorSubject<now.Timestamp>
  111. readonly reprCount: BehaviorSubject<number>
  112. readonly resized: BehaviorSubject<any>
  113. handleResize(): void
  114. /** Focuses camera on scene's bounding sphere, centered and zoomed. */
  115. requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
  116. readonly camera: Camera
  117. readonly boundingSphere: Readonly<Sphere3D>
  118. setProps(props: PartialCanvas3DProps | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void), doNotRequestDraw?: boolean /* = false */): void
  119. getImagePass(props: Partial<ImageProps>): ImagePass
  120. /** Returns a copy of the current Canvas3D instance props */
  121. readonly props: Readonly<Canvas3DProps>
  122. readonly input: InputObserver
  123. readonly stats: RendererStats
  124. readonly interaction: Canvas3dInteractionHelper['events']
  125. dispose(): void
  126. }
  127. const requestAnimationFrame = typeof window !== 'undefined'
  128. ? window.requestAnimationFrame
  129. : (f: (time: number) => void) => setImmediate(() => f(Date.now())) as unknown as number;
  130. const cancelAnimationFrame = typeof window !== 'undefined'
  131. ? window.cancelAnimationFrame
  132. : (handle: number) => clearImmediate(handle as unknown as NodeJS.Immediate);
  133. namespace Canvas3D {
  134. export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
  135. export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
  136. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
  137. export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ antialias: boolean, pixelScale: number, pickScale: number, enableWboit: boolean }> = {}) {
  138. const antialias = (attribs.antialias ?? true) && !attribs.enableWboit;
  139. const gl = getGLContext(canvas, {
  140. alpha: true,
  141. antialias,
  142. depth: true,
  143. preserveDrawingBuffer: true,
  144. premultipliedAlpha: true,
  145. });
  146. if (gl === null) throw new Error('Could not create a WebGL rendering context');
  147. const { pixelScale } = attribs;
  148. const input = InputObserver.fromElement(canvas, { pixelScale });
  149. const webgl = createContext(gl, { pixelScale });
  150. const passes = new Passes(webgl, attribs);
  151. if (isDebugMode) {
  152. const loseContextExt = gl.getExtension('WEBGL_lose_context');
  153. if (loseContextExt) {
  154. canvas.addEventListener('mousedown', e => {
  155. if (webgl.isContextLost) return;
  156. if (!e.shiftKey || !e.ctrlKey || !e.altKey) return;
  157. if (isDebugMode) console.log('lose context');
  158. loseContextExt.loseContext();
  159. setTimeout(() => {
  160. if (!webgl.isContextLost) return;
  161. if (isDebugMode) console.log('restore context');
  162. loseContextExt.restoreContext();
  163. }, 1000);
  164. }, false);
  165. }
  166. }
  167. // https://www.khronos.org/webgl/wiki/HandlingContextLost
  168. canvas.addEventListener('webglcontextlost', e => {
  169. webgl.setContextLost();
  170. e.preventDefault();
  171. if (isDebugMode) console.log('context lost');
  172. }, false);
  173. canvas.addEventListener('webglcontextrestored', () => {
  174. if (!webgl.isContextLost) return;
  175. webgl.handleContextRestored();
  176. if (isDebugMode) console.log('context restored');
  177. }, false);
  178. // disable postprocessing anti-aliasing if canvas anti-aliasing is enabled
  179. if (antialias && !props.postprocessing?.antialiasing) {
  180. props.postprocessing = {
  181. ...DefaultCanvas3DParams.postprocessing,
  182. antialiasing: { name: 'off', params: {} }
  183. };
  184. }
  185. return create(webgl, input, passes, props, { pixelScale });
  186. }
  187. export function create(webgl: WebGLContext, input: InputObserver, passes: Passes, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ pixelScale: number }>): Canvas3D {
  188. const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
  189. const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
  190. const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
  191. const reprCount = new BehaviorSubject(0);
  192. let startTime = now();
  193. const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  194. const commited = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  195. const { gl, contextRestored } = webgl;
  196. let x = 0;
  197. let y = 0;
  198. let width = 128;
  199. let height = 128;
  200. updateViewport();
  201. const scene = Scene.create(webgl);
  202. const camera = new Camera({
  203. position: Vec3.create(0, 0, 100),
  204. mode: p.camera.mode,
  205. fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
  206. clipFar: p.cameraClipping.far
  207. }, { x, y, width, height }, { pixelScale: attribs.pixelScale });
  208. const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
  209. const controls = TrackballControls.create(input, camera, p.trackball);
  210. const renderer = Renderer.create(webgl, p.renderer);
  211. const helper = new Helper(webgl, scene, p);
  212. const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height });
  213. const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera);
  214. const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
  215. let drawPending = false;
  216. let cameraResetRequested = false;
  217. let nextCameraResetDuration: number | undefined = void 0;
  218. let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0;
  219. let notifyDidDraw = true;
  220. function getLoci(pickingId: PickingId | undefined) {
  221. let loci: Loci = EmptyLoci;
  222. let repr: Representation.Any = Representation.Empty;
  223. if (pickingId) {
  224. loci = helper.handle.getLoci(pickingId);
  225. reprRenderObjects.forEach((_, _repr) => {
  226. const _loci = _repr.getLoci(pickingId);
  227. if (!isEmptyLoci(_loci)) {
  228. if (!isEmptyLoci(loci)) {
  229. console.warn('found another loci, this should not happen');
  230. }
  231. loci = _loci;
  232. repr = _repr;
  233. }
  234. });
  235. }
  236. return { loci, repr };
  237. }
  238. function mark(reprLoci: Representation.Loci, action: MarkerAction) {
  239. const { repr, loci } = reprLoci;
  240. let changed = false;
  241. if (repr) {
  242. changed = repr.mark(loci, action);
  243. } else {
  244. changed = helper.handle.mark(loci, action);
  245. reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
  246. }
  247. if (changed) {
  248. scene.update(void 0, true);
  249. helper.handle.scene.update(void 0, true);
  250. const prevPickDirty = pickHelper.dirty;
  251. draw(true);
  252. pickHelper.dirty = prevPickDirty; // marking does not change picking buffers
  253. }
  254. }
  255. function render(force: boolean) {
  256. if (webgl.isContextLost) return false;
  257. if (x > gl.drawingBufferWidth || x + width < 0 ||
  258. y > gl.drawingBufferHeight || y + height < 0
  259. ) return false;
  260. let didRender = false;
  261. controls.update(currentTime);
  262. const cameraChanged = camera.update();
  263. const multiSampleChanged = multiSampleHelper.update(force || cameraChanged, p.multiSample);
  264. if (force || cameraChanged || multiSampleChanged) {
  265. let cam: Camera | StereoCamera = camera;
  266. if (p.camera.stereo.name === 'on') {
  267. stereoCamera.update();
  268. cam = stereoCamera;
  269. }
  270. if (MultiSamplePass.isEnabled(p.multiSample)) {
  271. multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
  272. } else {
  273. passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing);
  274. }
  275. pickHelper.dirty = true;
  276. didRender = true;
  277. }
  278. return didRender;
  279. }
  280. let forceNextDraw = false;
  281. let forceDrawAfterAllCommited = false;
  282. let currentTime = 0;
  283. function draw(force?: boolean) {
  284. if (render(!!force || forceNextDraw) && notifyDidDraw) {
  285. didDraw.next(now() - startTime as now.Timestamp);
  286. }
  287. forceNextDraw = false;
  288. drawPending = false;
  289. }
  290. function requestDraw(force?: boolean) {
  291. if (drawPending) return;
  292. drawPending = true;
  293. forceNextDraw = !!force;
  294. }
  295. let animationFrameHandle = 0;
  296. function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }) {
  297. currentTime = t;
  298. commit(options?.isSynchronous);
  299. camera.transition.tick(currentTime);
  300. if (options?.manualDraw) {
  301. return;
  302. }
  303. draw(false);
  304. if (!camera.transition.inTransition && !webgl.isContextLost) {
  305. interactionHelper.tick(currentTime);
  306. }
  307. }
  308. function _animate() {
  309. tick(now());
  310. animationFrameHandle = requestAnimationFrame(_animate);
  311. }
  312. function resetTime(t: now.Timestamp) {
  313. startTime = t;
  314. controls.start(t);
  315. }
  316. function animate() {
  317. controls.start(now());
  318. if (animationFrameHandle === 0) _animate();
  319. }
  320. function pause() {
  321. cancelAnimationFrame(animationFrameHandle);
  322. animationFrameHandle = 0;
  323. }
  324. function identify(x: number, y: number): PickData | undefined {
  325. const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
  326. return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
  327. }
  328. function commit(isSynchronous: boolean = false) {
  329. const allCommited = commitScene(isSynchronous);
  330. // Only reset the camera after the full scene has been commited.
  331. if (allCommited) {
  332. resolveCameraReset();
  333. if (forceDrawAfterAllCommited) {
  334. if (helper.debug.isEnabled) helper.debug.update();
  335. draw(true);
  336. forceDrawAfterAllCommited = false;
  337. }
  338. commited.next(now());
  339. }
  340. }
  341. function resolveCameraReset() {
  342. if (!cameraResetRequested) return;
  343. const { center, radius } = scene.boundingSphereVisible;
  344. if (radius > 0) {
  345. const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration;
  346. const focus = camera.getFocus(center, radius);
  347. const snapshot = nextCameraResetSnapshot ? { ...focus, ...nextCameraResetSnapshot } : focus;
  348. camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration);
  349. }
  350. nextCameraResetDuration = void 0;
  351. nextCameraResetSnapshot = void 0;
  352. cameraResetRequested = false;
  353. }
  354. const oldBoundingSphereVisible = Sphere3D();
  355. const cameraSphere = Sphere3D();
  356. function shouldResetCamera() {
  357. if (camera.state.radiusMax === 0) return true;
  358. if (camera.transition.inTransition || nextCameraResetSnapshot) return false;
  359. let cameraSphereOverlapsNone = true;
  360. Sphere3D.set(cameraSphere, camera.state.target, camera.state.radius);
  361. // check if any renderable has moved outside of the old bounding sphere
  362. // and if no renderable is overlapping with the camera sphere
  363. for (const r of scene.renderables) {
  364. if (!r.state.visible) continue;
  365. const b = r.values.boundingSphere.ref.value;
  366. if (!b.radius) continue;
  367. const cameraDist = Vec3.distance(cameraSphere.center, b.center);
  368. if ((cameraDist > cameraSphere.radius || cameraDist > b.radius || b.radius > camera.state.radiusMax) && !Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
  369. if (Sphere3D.overlaps(cameraSphere, b)) cameraSphereOverlapsNone = false;
  370. }
  371. return cameraSphereOverlapsNone;
  372. }
  373. const sceneCommitTimeoutMs = 250;
  374. function commitScene(isSynchronous: boolean) {
  375. if (!scene.needsCommit) return true;
  376. // snapshot the current bounding sphere of visible objects
  377. Sphere3D.copy(oldBoundingSphereVisible, scene.boundingSphereVisible);
  378. if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
  379. if (helper.debug.isEnabled) helper.debug.update();
  380. if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
  381. cameraResetRequested = true;
  382. }
  383. if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
  384. camera.setState({ radiusMax: scene.boundingSphere.radius }, 0);
  385. reprCount.next(reprRenderObjects.size);
  386. if (isDebugMode) consoleStats();
  387. return true;
  388. }
  389. function consoleStats() {
  390. console.table(scene.renderables.map(r => ({
  391. drawCount: r.values.drawCount.ref.value,
  392. instanceCount: r.values.instanceCount.ref.value,
  393. materialId: r.materialId,
  394. })));
  395. console.log(webgl.stats);
  396. const { texture, attribute, elements } = webgl.resources.getByteCounts();
  397. console.log({
  398. texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
  399. attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
  400. elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
  401. });
  402. }
  403. function add(repr: Representation.Any) {
  404. registerAutoUpdate(repr);
  405. const oldRO = reprRenderObjects.get(repr);
  406. const newRO = new Set<GraphicsRenderObject>();
  407. repr.renderObjects.forEach(o => newRO.add(o));
  408. if (oldRO) {
  409. if (!SetUtils.areEqual(newRO, oldRO)) {
  410. newRO.forEach(o => { if (!oldRO.has(o)) scene.add(o); });
  411. oldRO.forEach(o => { if (!newRO.has(o)) scene.remove(o); });
  412. }
  413. } else {
  414. repr.renderObjects.forEach(o => scene.add(o));
  415. }
  416. reprRenderObjects.set(repr, newRO);
  417. scene.update(repr.renderObjects, false);
  418. forceDrawAfterAllCommited = true;
  419. if (isDebugMode) consoleStats();
  420. }
  421. function remove(repr: Representation.Any) {
  422. unregisterAutoUpdate(repr);
  423. const renderObjects = reprRenderObjects.get(repr);
  424. if (renderObjects) {
  425. renderObjects.forEach(o => scene.remove(o));
  426. reprRenderObjects.delete(repr);
  427. forceDrawAfterAllCommited = true;
  428. if (isDebugMode) consoleStats();
  429. }
  430. }
  431. function registerAutoUpdate(repr: Representation.Any) {
  432. if (reprUpdatedSubscriptions.has(repr)) return;
  433. reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
  434. if (!repr.state.syncManually) add(repr);
  435. }));
  436. }
  437. function unregisterAutoUpdate(repr: Representation.Any) {
  438. const updatedSubscription = reprUpdatedSubscriptions.get(repr);
  439. if (updatedSubscription) {
  440. updatedSubscription.unsubscribe();
  441. reprUpdatedSubscriptions.delete(repr);
  442. }
  443. }
  444. function getProps(): Canvas3DProps {
  445. const radius = scene.boundingSphere.radius > 0
  446. ? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100)
  447. : 0;
  448. return {
  449. camera: {
  450. mode: camera.state.mode,
  451. helper: { ...helper.camera.props },
  452. stereo: { ...p.camera.stereo },
  453. manualReset: !!p.camera.manualReset
  454. },
  455. cameraFog: camera.state.fog > 0
  456. ? { name: 'on' as const, params: { intensity: camera.state.fog } }
  457. : { name: 'off' as const, params: {} },
  458. cameraClipping: { far: camera.state.clipFar, radius },
  459. cameraResetDurationMs: p.cameraResetDurationMs,
  460. transparentBackground: p.transparentBackground,
  461. viewport: p.viewport,
  462. postprocessing: { ...p.postprocessing },
  463. multiSample: { ...p.multiSample },
  464. renderer: { ...renderer.props },
  465. trackball: { ...controls.props },
  466. debug: { ...helper.debug.props },
  467. handle: { ...helper.handle.props },
  468. };
  469. }
  470. const contextRestoredSub = contextRestored.subscribe(() => {
  471. pickHelper.dirty = true;
  472. draw(true);
  473. });
  474. const resized = new BehaviorSubject<any>(0);
  475. return {
  476. webgl,
  477. add,
  478. remove,
  479. commit,
  480. update: (repr, keepSphere) => {
  481. if (repr) {
  482. if (!reprRenderObjects.has(repr)) return;
  483. scene.update(repr.renderObjects, !!keepSphere);
  484. } else {
  485. scene.update(void 0, !!keepSphere);
  486. }
  487. forceDrawAfterAllCommited = true;
  488. },
  489. clear: () => {
  490. reprUpdatedSubscriptions.forEach(v => v.unsubscribe());
  491. reprUpdatedSubscriptions.clear();
  492. reprRenderObjects.clear();
  493. scene.clear();
  494. helper.debug.clear();
  495. requestDraw(true);
  496. reprCount.next(reprRenderObjects.size);
  497. },
  498. syncVisibility: () => {
  499. if (camera.state.radiusMax === 0) {
  500. cameraResetRequested = true;
  501. nextCameraResetDuration = 0;
  502. }
  503. if (scene.syncVisibility()) {
  504. if (helper.debug.isEnabled) helper.debug.update();
  505. }
  506. requestDraw(true);
  507. },
  508. requestDraw,
  509. tick,
  510. animate,
  511. resetTime,
  512. pause,
  513. identify,
  514. mark,
  515. getLoci,
  516. handleResize: () => {
  517. passes.updateSize();
  518. updateViewport();
  519. syncViewport();
  520. requestDraw(true);
  521. resized.next(+new Date());
  522. },
  523. requestCameraReset: options => {
  524. nextCameraResetDuration = options?.durationMs;
  525. nextCameraResetSnapshot = options?.snapshot;
  526. cameraResetRequested = true;
  527. },
  528. camera,
  529. boundingSphere: scene.boundingSphere,
  530. get notifyDidDraw() { return notifyDidDraw; },
  531. set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
  532. didDraw,
  533. commited,
  534. reprCount,
  535. resized,
  536. setProps: (properties, doNotRequestDraw = false) => {
  537. let props: PartialCanvas3DProps = typeof properties === 'function'
  538. ? produce(getProps(), properties)
  539. : properties;
  540. props = PD.normalizeParams(Canvas3DParams, props, false);
  541. const cameraState: Partial<Camera.Snapshot> = Object.create(null);
  542. if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
  543. cameraState.mode = props.camera.mode;
  544. }
  545. if (props.cameraFog !== undefined && props.cameraFog.params) {
  546. const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
  547. if (newFog !== camera.state.fog) cameraState.fog = newFog;
  548. }
  549. if (props.cameraClipping !== undefined) {
  550. if (props.cameraClipping.far !== undefined && props.cameraClipping.far !== camera.state.clipFar) {
  551. cameraState.clipFar = props.cameraClipping.far;
  552. }
  553. if (props.cameraClipping.radius !== undefined) {
  554. const radius = (scene.boundingSphere.radius / 100) * (100 - props.cameraClipping.radius);
  555. if (radius > 0 && radius !== cameraState.radius) {
  556. // if radius = 0, NaNs happen
  557. cameraState.radius = Math.max(radius, 0.01);
  558. }
  559. }
  560. }
  561. if (Object.keys(cameraState).length > 0) camera.setState(cameraState);
  562. if (props.camera?.helper) helper.camera.setProps(props.camera.helper);
  563. if (props.camera?.manualReset !== undefined) p.camera.manualReset = props.camera.manualReset;
  564. if (props.camera?.stereo !== undefined) Object.assign(p.camera.stereo, props.camera.stereo);
  565. if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
  566. if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
  567. if (props.viewport !== undefined) {
  568. const doNotUpdate = p.viewport === props.viewport ||
  569. (p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
  570. if (!doNotUpdate) {
  571. p.viewport = props.viewport;
  572. updateViewport();
  573. syncViewport();
  574. }
  575. }
  576. if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
  577. if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
  578. if (props.renderer) renderer.setProps(props.renderer);
  579. if (props.trackball) controls.setProps(props.trackball);
  580. if (props.debug) helper.debug.setProps(props.debug);
  581. if (props.handle) helper.handle.setProps(props.handle);
  582. if (cameraState.mode === 'orthographic') {
  583. p.camera.stereo.name = 'off';
  584. }
  585. if (!doNotRequestDraw) {
  586. requestDraw(true);
  587. }
  588. },
  589. getImagePass: (props: Partial<ImageProps> = {}) => {
  590. return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
  591. },
  592. get props() {
  593. return getProps();
  594. },
  595. get input() {
  596. return input;
  597. },
  598. get stats() {
  599. return renderer.stats;
  600. },
  601. get interaction() {
  602. return interactionHelper.events;
  603. },
  604. dispose: () => {
  605. contextRestoredSub.unsubscribe();
  606. scene.clear();
  607. helper.debug.clear();
  608. input.dispose();
  609. controls.dispose();
  610. renderer.dispose();
  611. interactionHelper.dispose();
  612. }
  613. };
  614. function updateViewport() {
  615. if (p.viewport.name === 'canvas') {
  616. x = 0;
  617. y = 0;
  618. width = gl.drawingBufferWidth;
  619. height = gl.drawingBufferHeight;
  620. } else {
  621. x = p.viewport.params.x * webgl.pixelRatio;
  622. y = p.viewport.params.y * webgl.pixelRatio;
  623. width = p.viewport.params.width * webgl.pixelRatio;
  624. height = p.viewport.params.height * webgl.pixelRatio;
  625. }
  626. }
  627. function syncViewport() {
  628. pickHelper.setViewport(x, y, width, height);
  629. renderer.setViewport(x, y, width, height);
  630. Viewport.set(camera.viewport, x, y, width, height);
  631. Viewport.set(controls.viewport, x, y, width, height);
  632. }
  633. }
  634. }