@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
633 lines (605 loc) • 19.4 kB
text/typescript
import {ApplicableControlsNode} from './../../core/camera/CoreCameraControlsController';
import {createRaycaster} from './../../core/RaycastHelper';
import {ViewerCamerasController} from './utils/ViewerCamerasController';
import {ViewerControlsController} from './utils/ViewerControlsController';
import {ViewerEventsController} from './utils/ViewerEventsController';
import {ViewerWebGLController} from './utils/ViewerWebglController';
import {ViewerAudioController} from './utils/ViewerAudioController';
import {Camera, Object3D, Vector2} from 'three';
import {PolyScene} from '../scene/PolyScene';
import {Poly, PolyEngine} from '../Poly';
import {AbstractRenderer} from './Common';
import {ViewerRaycastersController} from './utils/ViewerRaycastersController';
import {ViewerPerformanceMonitor} from './utils/ViewerPerformanceMonitor';
import {AvailableRenderConfig} from '../../core/camera/CoreCameraRendererController';
import {MouseHelper} from '../../core/event/MouseHelper';
const HOVERED_CLASS_NAME = 'hovered';
type ViewerTickCallback = (delta: number) => void;
type ViewerRenderCallback = (delta: number, renderer: AbstractRenderer) => void;
type ViewerBaseCallback = ViewerTickCallback | ViewerRenderCallback;
type OnResizeCallback = (width: number, height: number) => void;
interface BaseViewerCallbackOptions {
persistent?: boolean;
}
interface ViewerCallbackContainer<T extends ViewerBaseCallback> {
callback: T;
options: BaseViewerCallbackOptions;
}
type ViewerCallbacksMap<T extends ViewerBaseCallback> = Map<string, ViewerCallbackContainer<T>>;
export interface HTMLElementWithViewer<C extends Camera> extends HTMLElement {
scene: PolyScene;
viewer: TypedViewer<C>;
Poly: PolyEngine;
}
type UpdateCameraAspectCallback = (aspect: number, resolution: Vector2) => void;
export interface CreateViewerOptions {
canvas?: HTMLCanvasElement;
autoRender?: boolean;
renderer?: AbstractRenderer;
cameraMaskOverride?: string;
}
export interface TypedViewerOptions<C extends Camera> extends CreateViewerOptions {
camera: C;
scene: PolyScene;
updateCameraAspect: UpdateCameraAspectCallback;
}
export interface TypedViewerInterectionObserverChangeOptions {
root?: HTMLElement;
rootMargin?: string;
threshold?: number;
playPauseScene?: boolean;
onChange?: (isIntersecting: boolean) => void;
}
export interface BaseViewerMountOptions {
updateAutoRenderOnIntersectionChange?: boolean;
}
type RenderFuncWithDelta = (delta: number) => void;
type RenderFunc = () => void;
/**
*
*
* Base class to create a viewer. It is used for the [Threejs viewer](/docs/api/ThreejsViewer) as well as the [Mapbox Viewer](https://github.com/polygonjs/plugin-mapbox)
*
*/
export abstract class TypedViewer<C extends Camera> {
// protected _display_scene: Scene;
// protected _canvas: HTMLCanvasElement | undefined;
protected _domElement: HTMLElementWithViewer<C> | undefined;
protected _active: boolean = false;
private static _nextViewerId = 0;
private _id: Readonly<string>;
protected _renderObjectOverride: Object3D | undefined;
protected _canvas: HTMLCanvasElement | undefined;
protected _camera: C;
protected _scene: PolyScene;
public readonly updateCameraAspect: UpdateCameraAspectCallback;
protected _doRender: boolean = true;
protected _controlsNode: ApplicableControlsNode | undefined;
protected _renderFunc: RenderFuncWithDelta | undefined;
protected _renderCSSFunc: RenderFunc | undefined;
protected _renderer: AbstractRenderer | undefined;
constructor(options: TypedViewerOptions<C>) {
this._id = TypedViewer._nextId();
this._camera = options.camera;
this._scene = options.scene;
this._canvas = options.canvas;
if (options.autoRender != null) {
this._doRender = options.autoRender;
}
this.updateCameraAspect = options.updateCameraAspect;
this.scene().viewersRegister.registerViewer(this);
// init in constructor to ensure scene is present in viewer
this.raycastersController = new ViewerRaycastersController(this);
}
private static _nextId() {
return `${TypedViewer._nextViewerId++}`;
}
createRaycaster() {
return createRaycaster();
}
abstract renderer(): AbstractRenderer | undefined;
abstract rendererConfig(): AvailableRenderConfig | undefined;
protected _mounted = false;
/**
* mounts the viewer onto an element
*
*
*/
mount(element: HTMLElement, options?: BaseViewerMountOptions) {
let updateAutoRenderOnIntersectionChange = true;
if (options && options.updateAutoRenderOnIntersectionChange != null) {
updateAutoRenderOnIntersectionChange = options.updateAutoRenderOnIntersectionChange;
}
this._domElement = element as HTMLElementWithViewer<C>;
this._domElement.viewer = this;
this._domElement.scene = this._scene;
this._domElement.Poly = Poly;
if (updateAutoRenderOnIntersectionChange) {
this.updateAutoRenderOnIntersectionChange({playPauseScene: true});
}
this.controlsController().mount();
this._mounted = true;
}
/**
* unmounts the viewer
*
*
*/
unmount() {
if (!this._domElement) {
return;
}
// let childElement: Element | undefined;
// while ((childElement = this._domElement.children[0])) {
// this._domElement.removeChild(childElement);
// }
// when unmounting, it is very unsafe to remove all HTMLElements
// that have been created inside _domElement
// as those could have been created by an app specific code
// OR... those could have been created when an element is shared by multiple scenes
// at different times
this._audioController?.unmount();
const canvas = this.canvas();
canvas.parentElement?.removeChild(canvas);
this.eventsController().unmount();
this.controlsController().unmount();
this._mounted = false;
}
static _canvasIdPrefix() {
return 'TypedViewer';
}
static createCanvas(id?: string) {
id = id || TypedViewer._nextId();
const canvas = Poly.canvasRegister.findOrCreateCanvas();
canvas.id = `${this._canvasIdPrefix()}_${id}`;
canvas.style.display = 'block';
canvas.style.outline = 'none';
// we add 100% to the width and height
// to make it easy for the canvas to not grow larger
// than its container, without requiring css to enforce this
canvas.style.width = '100%';
canvas.style.height = '100%';
// This gets rid of an annoying popup showing an option to copy the image.
// when doing a long press on touch device
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
});
return canvas;
}
controlsNode() {
return this._controlsNode;
}
/**
* return the canvas and create one if none yet
*
*
*/
canvas() {
return (this._canvas = this._canvas || TypedViewer.createCanvas(this._id));
}
protected _tickAndRender(delta: number) {
this._tick(delta);
this.render(delta);
this._postRender(delta);
}
protected _tick(delta: number) {
// updating the camera controls
// must be done before the actor nodes are run.
// otherwise, we are bound to fetch an out of date camera matrix.
this.controlsController().update(delta);
//
this.raycastersController.updateRaycasters();
//
this._runOnBeforeTickCallbacks(delta);
this.scene().update(delta);
this._runOnAfterTickCallbacks(delta);
}
render(delta: number) {
if (!this._doRender) {
return;
}
// if (this._canvas) {
// super.render(delta);
const renderer = this._renderer;
if (!renderer) {
console.error('render: no renderer');
return;
}
this._runOnBeforeRenderCallbacks(delta, renderer);
if (this._renderFunc) {
this._renderFunc(delta);
}
if (this._renderCSSFunc) {
this._renderCSSFunc();
}
// this.controlsController().update(delta);
this._runOnAfterRenderCallbacks(delta, renderer);
// } else {
// console.warn('no canvas to render onto');
// }
}
protected _postRender(delta: number) {
this._scene.viewersRegister.markViewerAsRendered(this);
if (this.scene().timeController.playing()) {
this.performanceMonitor.measurePerformance(delta);
}
}
setRenderObjectOverride(object?: Object3D | null) {
if (object) {
this._renderObjectOverride = object;
} else {
this._renderObjectOverride = undefined;
}
}
active() {
return this._active;
}
activate() {
this._active = true;
}
deactivate() {
this._active = false;
}
protected _camerasController: ViewerCamerasController | undefined;
camerasController(): ViewerCamerasController {
return (this._camerasController = this._camerasController || new ViewerCamerasController(this));
}
protected _controlsController: ViewerControlsController<C> = new ViewerControlsController(this);
controlsController() {
return this._controlsController;
}
protected _eventsController: ViewerEventsController | undefined;
eventsController(): ViewerEventsController {
return (this._eventsController = this._eventsController || new ViewerEventsController(this));
}
protected _webGLController: ViewerWebGLController | undefined;
webglController(): ViewerWebGLController {
return (this._webGLController = this._webGLController || new ViewerWebGLController(this));
}
private _audioController: ViewerAudioController | undefined;
audioController(): ViewerAudioController {
return (this._audioController = this._audioController || new ViewerAudioController(this));
}
public readonly raycastersController: ViewerRaycastersController;
public readonly performanceMonitor: ViewerPerformanceMonitor = new ViewerPerformanceMonitor(this);
domElement() {
return this._domElement;
}
scene() {
return this._scene;
}
/**
* return the camera the viewer was created with
*
*
*/
camera() {
return this._camera;
}
// cameraControlsController(): ThreejsCameraControlsController | undefined {
// return undefined;
// }
id() {
return this._id;
}
protected _size = new Vector2();
size(target: Vector2) {
target.copy(this._size);
}
updateSize() {}
/**
* disposes the viewer
*
*
*/
dispose() {
this._scene.viewersRegister.unregisterViewer(this);
this.controlsController().unmount();
if (!this._domElement) {
return;
}
let child: Element;
while ((child = this._domElement.children[0])) {
this._domElement.removeChild(child);
}
}
// html container class
resetContainerClass() {
this.domElement()?.classList.remove(HOVERED_CLASS_NAME);
}
setContainerClassHovered() {
this.domElement()?.classList.add(HOVERED_CLASS_NAME);
}
markAsReady() {}
/**
* sets auto render state. If falls, the viewer will not render.
*
*
*/
setAutoRender(state = true) {
this._doRender = state;
}
autoRenderState(): boolean {
return this._doRender;
}
//
//
// Visibility detection
//
//
private _observer: IntersectionObserver | undefined;
/**
* This sets the viewer to detect if it is visible, and to pause/unpause itself when its visibility changes.
* This can be very useful to improve performance.
*
*/
updateAutoRenderOnIntersectionChange(_options: TypedViewerInterectionObserverChangeOptions) {
if (!this._domElement) {
console.warn('cannot apply');
return;
}
let {threshold, root, rootMargin, playPauseScene, onChange} = _options;
if (threshold == null) {
threshold = 0.01;
}
const observerOptions: IntersectionObserverInit = {
root,
rootMargin,
threshold,
};
const onObserverChange: IntersectionObserverCallback = (entries, observer) => {
if (!this._scene.loadingController.loaded()) {
// we must check that the scene is loaded before
// calling .play(),
// otherwise, the scene will start playing as soon
// as the viewer is created,
// even if we have the option .autoPlay=false in the loading options
return;
}
entries.forEach((entry) => {
const isVisible: boolean = entry.isIntersecting;
this.setAutoRender(isVisible);
if (playPauseScene) {
if (isVisible) {
this._scene.play();
} else {
this._scene.pause();
}
}
if (onChange) {
onChange(isVisible);
}
});
};
this.disableUpdateAutoRenderOnIntersectionChange();
this._observer = new IntersectionObserver(onObserverChange, observerOptions);
this._observer.observe(this._domElement);
}
/**
* Cancels the listening of the viewer's visibility.
*
*/
disableUpdateAutoRenderOnIntersectionChange() {
if (!this._domElement) {
return;
}
if (!this._observer) {
return;
}
this._observer.unobserve(this._domElement);
}
//
//
// RESIZE CALLBACKS
//
//
protected _onResizeCallbacks: OnResizeCallback[] | undefined;
registerOnResizeCallback(callback: OnResizeCallback) {
this._onResizeCallbacks = this._onResizeCallbacks || [];
this._onResizeCallbacks.push(callback);
}
unregisterOnResizeCallback(callback: OnResizeCallback) {
if (!this._onResizeCallbacks) {
return;
}
const index = this._onResizeCallbacks.indexOf(callback);
if (index >= 0) {
this._onResizeCallbacks.splice(index, 1);
}
if (this._onResizeCallbacks.length == 0) {
this._onResizeCallbacks = undefined;
}
}
protected _runOnResizeCallbacks() {
const canvas = this.canvas();
if (canvas) {
MouseHelper.resetCacheForCanvas(canvas);
}
if (!this._onResizeCallbacks) {
return;
}
for (const callback of this._onResizeCallbacks) {
callback(this._size.x, this._size.y);
}
}
//
//
// CALLBACKS
//
//
// tick callbacks
private _onBeforeTickCallbacksMap: ViewerCallbacksMap<ViewerTickCallback> | undefined;
private _onAfterTickCallbacksMap: ViewerCallbacksMap<ViewerTickCallback> | undefined;
protected _onBeforeTickCallbacks: Array<ViewerTickCallback> = [];
protected _onAfterTickCallbacks: Array<ViewerTickCallback> = [];
// render callbacks
private _onBeforeRenderCallbacksMap: ViewerCallbacksMap<ViewerRenderCallback> | undefined;
private _onAfterRenderCallbacksMap: ViewerCallbacksMap<ViewerRenderCallback> | undefined;
protected _onBeforeRenderCallbacks: Array<ViewerRenderCallback> = [];
protected _onAfterRenderCallbacks: Array<ViewerRenderCallback> = [];
// onBeforeTick
/**
* registers a BeforeTick callback. BeforeTick callbacks are run before updating the frame (and therefore before any time dependent node has changed)
*
*/
registerOnBeforeTick(callbackName: string, callback: ViewerTickCallback, options: BaseViewerCallbackOptions = {}) {
this._registerCallback(callbackName, callback, this.registeredBeforeTickCallbacks(), options);
}
/**
* unregisters BeforeTick callback
*
*/
unRegisterOnBeforeTick(callbackName: string) {
this._unregisterCallback(callbackName, this._onBeforeTickCallbacksMap);
}
/**
* Returns the list registered BeforeTick callback names
*
*/
registeredBeforeTickCallbacks() {
return (this._onBeforeTickCallbacksMap = this._onBeforeTickCallbacksMap || new Map());
}
// onAfterTick
/**
* registers AfterTick callback. AfterTick callbacks are run after updating the frame (and therefore after any time dependent node has changed)
*
*/
registerOnAfterTick(callbackName: string, callback: ViewerTickCallback, options: BaseViewerCallbackOptions = {}) {
this._registerCallback(callbackName, callback, this.registeredAfterTickCallbacks(), options);
}
/**
* unregisters AfterTick callback
*
*/
unRegisterOnAfterTick(callbackName: string) {
this._unregisterCallback(callbackName, this._onAfterTickCallbacksMap);
}
/**
* Returns the list registered AfterTick callback names
*
*/
registeredAfterTickCallbacks() {
return (this._onAfterTickCallbacksMap = this._onAfterTickCallbacksMap || new Map());
}
// onBeforeRender
/**
* registers a BeforeRender callback. BeforeRender callbacks are run before the frame is rendered
*
*/
registerOnBeforeRender(
callbackName: string,
callback: ViewerRenderCallback,
options: BaseViewerCallbackOptions = {}
) {
this._registerCallback(callbackName, callback, this.registeredBeforeRenderCallbacks(), options);
}
/**
* unregisters BeforeRender callback
*
*/
unRegisterOnBeforeRender(callbackName: string) {
this._unregisterCallback(callbackName, this._onBeforeRenderCallbacksMap);
}
/**
* Returns the list registered BeforeRender callback names
*
*/
registeredBeforeRenderCallbacks() {
return (this._onBeforeRenderCallbacksMap = this._onBeforeRenderCallbacksMap || new Map());
}
// onAfterRender
/**
* registers a AfterRender callback. AfterRender callbacks are run after the frame is rendered
*
*/ registerOnAfterRender(
callbackName: string,
callback: ViewerRenderCallback,
options: BaseViewerCallbackOptions = {}
) {
this._registerCallback(callbackName, callback, this.registeredAfterRenderCallbacks(), options);
}
/**
* unregisters AfterRender callback
*
*/
unRegisterOnAfterRender(callbackName: string) {
this._unregisterCallback(callbackName, this._onAfterRenderCallbacksMap);
}
/**
* Returns the list AfterRender BeforeRender callback names
*
*/
registeredAfterRenderCallbacks() {
return (this._onAfterRenderCallbacksMap = this._onAfterRenderCallbacksMap || new Map());
}
private _registerCallback<C extends ViewerBaseCallback>(
callbackName: string,
callback: C,
map: ViewerCallbacksMap<C>,
options: BaseViewerCallbackOptions = {}
) {
if (map.has(callbackName)) {
console.warn(`callback ${callbackName} already registered`);
return;
}
map.set(callbackName, {callback, options});
this._updateCallbacks();
}
private _unregisterCallback<C extends ViewerBaseCallback>(callbackName: string, map?: ViewerCallbacksMap<C>) {
if (!map) {
return;
}
const callbackContainer = map.get(callbackName);
if (!callbackContainer) {
return;
}
const options = callbackContainer.options;
if (options.persistent == true) {
return;
}
map.delete(callbackName);
this._updateCallbacks();
}
private _updateCallbacks() {
this._onBeforeTickCallbacks = [];
this._onBeforeTickCallbacksMap?.forEach((callbackContainer) => {
this._onBeforeTickCallbacks.push(callbackContainer.callback);
});
this._onAfterTickCallbacks = [];
this._onAfterTickCallbacksMap?.forEach((callbackContainer) => {
this._onAfterTickCallbacks.push(callbackContainer.callback);
});
this._onBeforeRenderCallbacks = [];
this._onBeforeRenderCallbacksMap?.forEach((callbackContainer) => {
this._onBeforeRenderCallbacks.push(callbackContainer.callback);
});
this._onAfterRenderCallbacks = [];
this._onAfterRenderCallbacksMap?.forEach((callbackContainer) => {
this._onAfterRenderCallbacks.push(callbackContainer.callback);
});
}
private _runTickCallbacks(callbacks: ViewerTickCallback[], delta: number) {
for (const callback of callbacks) {
callback(delta);
}
}
private _runRenderCallbacks(callbacks: ViewerRenderCallback[], delta: number, renderer: AbstractRenderer) {
for (const callback of callbacks) {
callback(delta, renderer);
}
}
protected _runOnBeforeTickCallbacks(delta: number) {
this._runTickCallbacks(this._onBeforeTickCallbacks, delta);
}
protected _runOnAfterTickCallbacks(delta: number) {
this._runTickCallbacks(this._onAfterTickCallbacks, delta);
}
protected _runOnBeforeRenderCallbacks(delta: number, renderer: AbstractRenderer) {
this._runRenderCallbacks(this._onBeforeRenderCallbacks, delta, renderer);
}
protected _runOnAfterRenderCallbacks(delta: number, renderer: AbstractRenderer) {
this._runRenderCallbacks(this._onAfterRenderCallbacks, delta, renderer);
}
}
export type BaseViewerType = TypedViewer<Camera>;