@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
489 lines (461 loc) • 14.5 kB
text/typescript
import {ActorsManager} from './utils/ActorsManager';
import {SceneCamerasController} from './utils/SceneCamerasController';
import {Cooker} from './utils/Cooker';
import {SceneCookController} from './utils/CookController';
import {CoreGraph} from '../../core/graph/CoreGraph';
import {CorePerformance} from '../../core/performance/CorePerformance';
import {DispatchController} from './utils/DispatchController';
import {SceneExpressionsController} from './utils/SceneExpressionsController';
import {SceneLifeCycleController} from './utils/LifeCycleController';
import {LoadingController} from './utils/LoadingController';
import {MissingReferencesController} from './utils/missingReferences/MissingReferencesController';
import {GraphNodesController} from './utils/GraphNodesController';
import {NodesController} from './utils/NodesController';
// import {PolySceneSerializer} from './utils/Serializer';
import {SceneEventsDispatcher} from './utils/events/EventsDispatcher';
import {ObjectsController} from './utils/ObjectsController';
import {ScenePerformanceMonitor} from './utils/ScenePerformanceMonitor';
import {ReferencesController} from './utils/ReferencesController';
import {
onTimeTickHook,
TimeController,
TimeControllerUpdateTimeOptions,
TIME_CONTROLLER_UPDATE_TIME_OPTIONS_DEFAULT,
} from './utils/TimeController';
import {UniformsController} from './utils/UniformsController';
import {ViewersRegister} from './utils/ViewersRegister';
import {SceneWebGLController} from './utils/WebGLController';
import {WindowController} from './utils/WindowController';
import {SceneAssetsController} from './utils/AssetsController';
import {SceneTraverserController} from './utils/SceneTraverser';
import {BaseNodeType} from '../nodes/_Base';
import {ObjNodeChildrenMap} from '../poly/registers/nodes/Obj';
import {Constructor, valueof} from '../../types/GlobalTypes';
import {Raycaster, Scene, WebGLRenderer} from 'three';
import {CoreString} from '../../core/String';
import {SceneRenderersRegister, RegisterRendererOptions} from './utils/SceneRenderersRegister';
import {Poly} from '../Poly';
import {NodeCreateOptions} from '../nodes/utils/hierarchy/ChildrenController';
import {SceneWebXRController} from './utils/WebXREventsController';
import {CoreObjectType, ObjectContent} from '../../core/geometry/ObjectContent';
import {CoreParamSerializer, BaseParamSerializer} from '../params/utils/CoreParamSerializer';
import type {ParamType} from '../poly/ParamType';
interface PolySceneCreateOptions {
root: NodeCreateOptions;
paramsSerializerClass?: typeof CoreParamSerializer<ParamType>;
}
type SceneBatchUpdateCallback = () => void | Promise<void>;
interface UpdateState {
scene: Scene;
}
/**
*
*
* PolyScene contains all nodes within a scene.
*
*/
export class PolyScene {
/**
* Returns the THREE.Scene.
*
* @remarks
*
* Read more about how to use a THREE.Scene on [THREE's documentation](https://threejs.org/docs/?q=scene#api/en/scenes/Scene)
*
*/
threejsScene(): Scene {
return this.root().object;
}
private _uuid!: string;
setUuid(uuid: string) {
return (this._uuid = uuid);
}
get uuid() {
return this._uuid;
}
private _name: string | undefined;
setName(newName: string) {
return (this._name = PolyScene.sanitizeName(newName));
}
static sanitizeName(newName: string) {
newName = CoreString.sanitizeName(newName);
// force lower case for now, as the editor export uses this
// and it may be safer to use lowercase to make it work across OSes
newName = newName.toLowerCase();
return newName;
}
name() {
return this._name;
}
protected _camerasController: SceneCamerasController | undefined;
get camerasController() {
return (this._camerasController = this._camerasController || new SceneCamerasController(this));
}
/**
* Returns the camera object that has been set as main
*
*/
mainCamera() {
return this.camerasController.mainCamera();
}
private _cooker = new Cooker(this);
get cooker() {
return this._cooker;
}
// private _cube_cameras_controller: CubeCamerasController;
// get cube_cameras_controller() {
// return (this._cube_cameras_controller = this._cube_cameras_controller || new CubeCamerasController(this));
// }
private _actorsManager: ActorsManager | undefined;
get actorsManager() {
return (this._actorsManager = this._actorsManager || new ActorsManager(this));
}
private _assetsController: SceneAssetsController | undefined;
get assets() {
return (this._assetsController = this._assetsController || new SceneAssetsController());
}
public readonly cookController = new SceneCookController();
/**
* Returns a promise to wait for all nodes to have cooked when loading a scene.
*
*/
async waitForCooksCompleted(): Promise<void> {
return await this.cookController.waitForCooksCompleted();
}
private _dispatchController: DispatchController | undefined;
get dispatchController() {
return (this._dispatchController = this._dispatchController || new DispatchController(this));
}
private _eventsDispatcher: SceneEventsDispatcher | undefined;
get eventsDispatcher() {
return (this._eventsDispatcher = this._eventsDispatcher || new SceneEventsDispatcher(this));
}
private _webXRController: SceneWebXRController | undefined;
get webXR() {
return (this._webXRController = this._webXRController || new SceneWebXRController(this));
}
/**
* When using Polygonjs viewers, a raycaster is created to use mouse events and detect if there are any object under the cursor.
* But if no viewer is created, such as when [importing a scene in react three fiber](/docs/integrations/react_three_fiber),
* It is then useful to give a raycaster.
*
*/
setRaycaster(raycaster: Raycaster) {
this.eventsDispatcher.setRaycaster(raycaster);
}
private _graph = new CoreGraph();
get graph() {
return this._graph;
}
public readonly lifecycleController: SceneLifeCycleController = new SceneLifeCycleController(this);
public readonly loadingController: LoadingController = new LoadingController(this);
public readonly missingExpressionReferencesController: MissingReferencesController =
new MissingReferencesController(this);
public readonly expressionsController: SceneExpressionsController = new SceneExpressionsController();
public readonly nodesController = new NodesController(this);
public readonly graphNodesController = new GraphNodesController(this);
public readonly renderersRegister = new SceneRenderersRegister(this);
/**
* Creates a new node.
*
* nodeClass can be either a string or a node class. Both examples below work:
*
* - polyScene.createNode('box'): returns a BoxSopNode
* - polyScene.createNode(boxSopNode): returns a BoxSopNode
*
*/
createNode<S extends keyof ObjNodeChildrenMap>(nodeClass: S, options?: NodeCreateOptions): ObjNodeChildrenMap[S];
createNode<K extends valueof<ObjNodeChildrenMap>>(nodeClass: Constructor<K>, options?: NodeCreateOptions): K;
createNode<K extends valueof<ObjNodeChildrenMap>>(nodeClass: Constructor<K>, options?: NodeCreateOptions): K {
return this.root().createNode(nodeClass, options) as K;
}
/**
* returns all nodes with a given type
*
* - polyScene.nodesByType('box'): returns all BoxSopNodes
*/
nodesByType(type: string): BaseNodeType[] {
return this.nodesController.nodesByType(type);
}
protected _objectsController = new ObjectsController(this);
get objectsController() {
return this._objectsController;
}
/**
* returns a THREE.Object3D whose name matches the mask
*
*/
findObjectByMask<T extends CoreObjectType>(mask: string): ObjectContent<T> | undefined {
return this._objectsController.findObjectByMask(mask);
}
/**
* returns a list THREE.Object3Ds whose names matche the mask
*
*/
objectsByMask<T extends CoreObjectType>(mask: string, parent?: ObjectContent<T>): ObjectContent<T>[] {
return this._objectsController.objectsByMask(mask, parent);
}
protected _referencesController = new ReferencesController(this);
get referencesController() {
return this._referencesController;
}
protected _performance: CorePerformance | undefined;
get performance() {
return (this._performance = this._performance || new CorePerformance());
}
public readonly perfMonitor = new ScenePerformanceMonitor(this);
protected _viewersRegister: ViewersRegister | undefined;
get viewersRegister() {
return (this._viewersRegister = this._viewersRegister || new ViewersRegister(this));
}
public readonly sceneTraverser = new SceneTraverserController(this);
/**
* updates Polygonjs scene internals. This is called automatically when using Polygonjs viewers,
* but you would need to call it yourself in the render loop when adding your scene to threejs or react-three-fiber.
* See [https://polygonjs.com/docs/integrations](https://polygonjs.com/docs/integrations)
*
*/
update(delta: number, state?: UpdateState) {
// setDelta is necessary here, as this function is most likely called from an integration with threejs, using a custom render loop
this.timeController.setDelta(delta);
this.timeController.incrementTimeIfPlaying(TIME_CONTROLLER_UPDATE_TIME_OPTIONS_DEFAULT);
this.sceneTraverser.traverseScene(state?.scene);
}
//
//
// time
//
//
readonly timeController = new TimeController(this);
/**
* sets the current frame
*
*/
setFrame(frame: number) {
this.timeController.setFrame(frame);
}
setFrameToStart() {
this.timeController.setFrameToStart();
}
/**
* returns the current frame
*
*/
frame(): number {
return this.timeController.frame();
}
/**
* returns the current time
*
*/
time(): number {
return this.timeController.time();
}
maxFrame() {
return this.timeController.maxFrame();
}
/**
* starts playing the scene
*
*/
play() {
this.timeController.play();
}
/**
* pauses the scene
*
*/
pause() {
this.timeController.pause();
}
/**
* increments the time
*
*/
incrementTime(options?: TimeControllerUpdateTimeOptions) {
this.timeController.incrementTime(options);
}
/**
* increments the time if the scene is playing()
*
*/
incrementTimeIfPlaying(options?: TimeControllerUpdateTimeOptions) {
this.timeController.incrementTimeIfPlaying(options);
}
/**
* registers a renderer
*
*/
registerRenderer(renderer: WebGLRenderer, options?: RegisterRendererOptions) {
return this.renderersRegister.registerRenderer(renderer, options);
}
//
//
// uniforms
//
//
private _uniformsController: UniformsController | undefined;
get uniformsController() {
return (this._uniformsController = this._uniformsController || new UniformsController(this));
}
//
//
// webgl
//
//
private _webglController: SceneWebGLController | undefined;
get webglController() {
return (this._webglController = this._webglController || new SceneWebGLController());
}
//
//
// window
//
//
private _windowController: WindowController | undefined;
get windowController() {
return (this._windowController = this._windowController || new WindowController(this));
}
//
//
// constructor
//
//
constructor(options?: PolySceneCreateOptions) {
this._graph.setScene(this);
this._paramSerializerClass = options?.paramsSerializerClass;
this.nodesController.createRoot(options?.root);
Poly.scenesRegister.registerScene(this);
}
private _disposed = false;
dispose() {
if (this._disposed == true) {
return;
}
this._disposed = true;
this.batchUpdates(() => {
this.nodesController.traverseNodes((node) => {
node.parent()?.removeNode(node);
});
});
if (this._windowController) {
this._windowController.dispose();
this._windowController = undefined;
}
this.timeController.dispose();
this.renderersRegister.dispose();
this.camerasController.dispose();
this.root().dispose();
Poly.scenesRegister.deregisterScene(this);
}
disposed() {
return this._disposed;
}
private _paramSerializerClass?: BaseParamSerializer;
paramSerializerClass() {
return this._paramSerializerClass;
}
//
//
// cooker
//
//
/**
* batchUpdates can be useful to set multiple parameter values without triggering a recook for each update.
*
*/
async batchUpdates(callback: SceneBatchUpdateCallback) {
this._cooker.block();
await callback();
this._cooker.unblock();
}
//
//
// nodes
//
//
/**
* returns a node based on its path
*
* - polyScene.node('/geo1')
*
*/
node(path: string) {
return this.nodesController.node(path);
}
/**
* returns the root node
*
*/
root() {
return this.nodesController.root();
}
/**
* traverse all nodes and runs a callback for each
*
*/
traverseNodes(callback: (node: BaseNodeType) => void) {
this.nodesController.traverseNodes(callback);
}
//
//
// CALLBACKS
//
//
/**
* 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: onTimeTickHook): void {
this.timeController.registerOnBeforeTick(callbackName, callback);
}
/**
* unregisters BeforeTick callback
*
*/
unRegisterOnBeforeTick(callbackName: string): void {
this.timeController.unRegisterOnBeforeTick(callbackName);
}
/**
* Returns the list registered BeforeTick callback names
*
*/
registeredBeforeTickCallbacks(): Map<string, onTimeTickHook> {
return this.timeController.registeredBeforeTickCallbacks();
}
/**
* return true if a callback is registered with that name
*
*/
hasBeforeTickCallback(callbackName: string): boolean {
return this.timeController.hasBeforeTickCallback(callbackName);
}
/**
* registers AfterTick callback. AfterTick callbacks are run after updating the frame (and therefore after any time dependent node has changed)
*
*/
registerOnAfterTick(callbackName: string, callback: onTimeTickHook): void {
this.timeController.registerOnAfterTick(callbackName, callback);
}
/**
* unregisters AfterTick callback
*
*/
unRegisterOnAfterTick(callbackName: string): void {
this.timeController.unRegisterOnAfterTick(callbackName);
}
/**
* Returns the list registered AfterTick callback names
*
*/
registeredAfterTickCallbacks(): Map<string, onTimeTickHook> {
return this.timeController.registeredAfterTickCallbacks();
}
/**
* return true if a callback is registered with that name
*
*/
hasAfterTickCallback(callbackName: string): boolean {
return this.timeController.hasAfterTickCallback(callbackName);
}
}