UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

581 lines (535 loc) 18.3 kB
import {BaseNodeType} from '../../_Base'; import {BaseParamType} from '../../../params/_Base'; import {ParamOptions} from '../../../params/utils/OptionsController'; import {CoreGraphNode} from '../../../../core/graph/CoreGraphNode'; import {FloatParam} from '../../../params/Float'; import {ParamType} from '../../../poly/ParamType'; import {ParamTemplate} from './ParamsConfig'; import {ParamConstructorMap} from '../../../params/types/ParamConstructorMap'; import {ParamConstructorByType} from '../../../params/types/ParamConstructorByType'; import {ParamInitValuesTypeMap} from '../../../params/types/ParamInitValuesTypeMap'; import {ParamValuesTypeMap} from '../../../params/types/ParamValuesTypeMap'; import {NodeEvent} from '../../../poly/NodeEvent'; import {ParamInitValueSerializedTypeMap} from '../../../params/types/ParamInitValueSerializedTypeMap'; import {Poly} from '../../../Poly'; import {ParamInitData} from '../io/IOController'; import {PolyDictionary} from '../../../../types/GlobalTypes'; import {arrayCopy} from '../../../../core/ArrayUtils'; const NODE_SIMPLE_NAME = 'params'; export type OnSceneLoadHook = () => void; type PostCreateParamsHook = () => void; export interface ParamOptionToAdd<T extends ParamType> { name: string; type: T; initValue: ParamInitValueSerializedTypeMap[T]; rawInput: ParamInitValueSerializedTypeMap[T]; options?: ParamOptions; } export interface ParamsUpdateOptions { namesToDelete?: string[]; toAdd?: ParamOptionToAdd<ParamType>[]; } const _tmpAll: BaseParamType[] = []; export class ParamsController { private _param_create_mode: boolean = false; private _params_created: boolean = false; private _paramsByName: Map<string, BaseParamType> = new Map(); // caches private _paramsList: BaseParamType[] = []; private _paramNames: string[] = []; private _non_spare_params: BaseParamType[] = []; private _spare_params: BaseParamType[] = []; private _non_spare_param_names: string[] = []; private _spare_param_names: string[] = []; private _params_node: CoreGraphNode | undefined; // private _params_eval_key: string; private _params_added_since_last_params_eval: boolean = false; // private _current_param_folder_name: string | undefined; // hooks private _post_create_params_hook_names: string[] | undefined; private _post_create_params_hooks: PostCreateParamsHook[] | undefined; private _on_scene_load_hooks: OnSceneLoadHook[] | undefined; private _on_scene_load_hook_names: string[] | undefined; constructor(public readonly node: BaseNodeType) {} dispose() { if (this._params_node) { this._params_node.dispose(); } // dispose params arrayCopy(this.all, _tmpAll); for (const param of _tmpAll) { param.dispose(); } // hooks this._post_create_params_hook_names = undefined; this._post_create_params_hooks = undefined; this._on_scene_load_hooks = undefined; this._on_scene_load_hook_names = undefined; } private initDependencyNode() { if (!this._params_node) { // TODO: consider not having a params_node for nodes which have no parameters this._params_node = new CoreGraphNode(this.node.scene(), NODE_SIMPLE_NAME); // this._params_node.setScene(this.node.scene); this.node.addGraphInput(this._params_node, false); } } init() { this.initDependencyNode(); // this.reset_params() this._param_create_mode = true; this._initFromParamsConfig(); this.node.createParams(); this._postCreateParams(); } private _postCreateParams() { this._updateCaches(); // this._create_params_ui_data_dependencies(); this._initParamAccessors(); this._param_create_mode = false; this._params_created = true; this._runPostCreateParamsHooks(); // This was to debug a weird bug where I was adding nodes to the list // of params, from the DependenciesController // this._params_list.push = (...items: BaseParamType[]) => { // if (items[0] && !items[0].compute) { // Poly.warn('adding params', items); // } // for (let i of items) { // this._params_list[this._params_list.length] = i; // } // return 0; // }; } postCreateSpareParams() { this._updateCaches(); this._initParamAccessors(); // param.emit(ParamEvent.DELETED); this.node.scene().referencesController.notifyParamsUpdated(this.node); this.node.emit(NodeEvent.PARAMS_UPDATED); } updateParams(options: ParamsUpdateOptions) { let hasCreatedAParam = false; let hasDeletedAParam = false; const addedParams: BaseParamType[] = []; if (options.namesToDelete) { for (const param_name of options.namesToDelete) { if (this.has(param_name)) { this._deleteParam(param_name); hasDeletedAParam = true; } } } if (options.toAdd) { for (const paramData of options.toAdd) { const param = this.addParam(paramData.type, paramData.name, paramData.initValue, paramData.options); if (param) { if (paramData.rawInput != null) { param.set(paramData.rawInput as never); } hasCreatedAParam = true; addedParams.push(param); } } } if (hasDeletedAParam || hasCreatedAParam) { this.postCreateSpareParams(); } for (const param of addedParams) { this.node.scene().missingExpressionReferencesController.checkForMissingParamReferences(param); } } private _initFromParamsConfig() { const paramsConfig = this.node.paramsConfig as PolyDictionary<ParamTemplate<ParamType>>; let init_values_used = false; if (paramsConfig) { const paramsInitValueOverrides = this.node.createOptions?.paramsInitValueOverrides; const names = Object.keys(paramsConfig); for (const name of names) { const config = paramsConfig[name]; let init_value: ParamInitData<ParamType> | undefined; if (paramsInitValueOverrides) { init_value = paramsInitValueOverrides[name]; init_values_used = true; } this.addParam(config.type, name, config.init_value, config.options, init_value); } } // this set dirty may not be necessary, but when starting a scene with a spotlight // with a non default t (ie: [2,2,0]), it would not be positionned correctly and would require // a cook if (init_values_used) { this.node.setDirty(); } if (this.node.createOptions) { this.node.createOptions.paramsInitValueOverrides = undefined; } } private _initParamAccessors() { let currentNamesInAccessor = Object.getOwnPropertyNames(this.node.pv); this._removeUnneededAccessors(currentNamesInAccessor); // update var after having removed accessors currentNamesInAccessor = Object.getOwnPropertyNames(this.node.pv); const all = this.all; for (const param of all) { const isSpare: boolean = param.options.isSpare(); const paramNotYetInAccessors = !currentNamesInAccessor.includes(param.name()); try { // this currently fails // when copy/pasting the js/getObjectAttribute node if (paramNotYetInAccessors || isSpare) { Object.defineProperty(this.node.pv, param.name(), { get: () => { return param.value; }, // only spare parameters can be removed configurable: isSpare, }); Object.defineProperty(this.node.p, param.name(), { get: () => { return param; }, configurable: isSpare, }); } } catch (err) {} } } private _removeUnneededAccessors(current_names_in_accessor: string[]) { const current_param_names = this._paramNames; const names_to_remove = []; for (const current_name_in_accessor of current_names_in_accessor) { if (!current_param_names.includes(current_name_in_accessor)) { names_to_remove.push(current_name_in_accessor); } } for (const name_to_remove of names_to_remove) { Object.defineProperty(this.node.pv, name_to_remove, { get: () => { return undefined; }, configurable: true, }); Object.defineProperty(this.node.p, name_to_remove, { get: () => { return undefined; }, configurable: true, }); } } get params_node() { return this._params_node; } get all() { return this._paramsList; } get non_spare() { return this._non_spare_params; } get spare() { return this._spare_params; } get names(): string[] { return this._paramNames; } get non_spare_names(): string[] { return this._non_spare_param_names; } get spare_names(): string[] { return this._spare_param_names; } private set_with_type<T extends ParamType>(paramName: string, value: ParamInitValuesTypeMap[T], type: T) { const param = this.paramWithType(paramName, type); if (param) { param.set(value as never); } else { Poly.warn(`param ${paramName} not found with type ${type}`); } } set_float(paramName: string, value: ParamInitValuesTypeMap[ParamType.FLOAT]) { this.set_with_type(paramName, value, ParamType.FLOAT); } set_vector3(paramName: string, value: ParamInitValuesTypeMap[ParamType.VECTOR3]) { this.set_with_type(paramName, value, ParamType.VECTOR3); } has_param(paramName: string) { return this._paramsByName.has(paramName); } has(paramName: string) { return this.has_param(paramName); } get(paramName: string) { return this.param(paramName); } paramWithType<T extends ParamType>(param_name: string, type: T): ParamConstructorMap[T] | undefined { const param = this.param(param_name); if (param && param.type() == type) { return param as ParamConstructorMap[T]; } } getFloat(param_name: string): FloatParam { return this.paramWithType(param_name, ParamType.FLOAT) as FloatParam; } // get_operator_path(param_name: string): OperatorPathParam { // return this.paramWithType(param_name, ParamType.OPERATOR_PATH) as OperatorPathParam; // } value(paramName: string) { return this.param(paramName)?.value; } valueWithType<T extends ParamType>(paramName: string, type: T): ParamValuesTypeMap[T] { return this.paramWithType(paramName, type)?.value as ParamValuesTypeMap[T]; // const param = this.param(name); // if (param && param.type() == type) { // return param.value(); // } } boolean(paramName: string) { return this.valueWithType(paramName, ParamType.BOOLEAN); } float(paramName: string) { return this.valueWithType(paramName, ParamType.FLOAT); } integer(paramName: string) { return this.valueWithType(paramName, ParamType.INTEGER); } string(paramName: string) { return this.valueWithType(paramName, ParamType.STRING); } vector2(paramName: string) { return this.valueWithType(paramName, ParamType.VECTOR2); } vector3(paramName: string) { return this.valueWithType(paramName, ParamType.VECTOR3); } color(paramName: string) { return this.valueWithType(paramName, ParamType.COLOR); } param(paramName: string) { const p = this._paramsByName.get(paramName); if (p != null) { return p; } else { Poly.warn( `tried to access param '${paramName}' in node ${this.node.path()}, but existing params are: ${ this.names } on node ${this.node.path()}` ); return null; } } // param_cache_name(param_name: string) { // return `_param_${param_name}`; // } // delete_params(param_names: string[]) { // for (let param_name of param_names) { // this.delete_param(param_name); // } // } // call update_params instead private _deleteParam(paramName: string) { const param = this._paramsByName.get(paramName); if (param) { if (this._params_node) { this._params_node.removeGraphInput(param); } param._setupNodeDependencies(null); this._paramsByName.delete(paramName); if (param.isMultiple() && param.components) { for (const component of param.components) { const childName = component.name(); this._paramsByName.delete(childName); } } param.dispose(); param.scene().graphNodesController.notifyParamPathChanged(param); // const name_index = this._param_names.indexOf(param_name) // if(name_index >= 0){ // this._param_names.splice(name_index, 1) // } // param.emit(ParamEvent.DELETED); } else { throw new Error(`param '${paramName}' does not exist on node ${this.node.path()}`); } } addParam<T extends ParamType>( type: T, paramName: string, defaultValue: ParamInitValuesTypeMap[T], options: ParamOptions = {}, initData?: ParamInitData<T> ): ParamConstructorMap[T] | undefined { const isSpare = options['spare'] || false; if (this._param_create_mode === false && !isSpare) { Poly.warn( `node ${this.node.path()} (${this.node.type()}) param '${paramName}' cannot be created outside of createParams` ); } if (this.node.scene() == null) { Poly.warn(`node ${this.node.path()} (${this.node.type()}) has no scene assigned`); } const constructor = ParamConstructorByType[type]; if (constructor != null) { const existing_param = this._paramsByName.get(paramName); if (existing_param) { if (isSpare) { // delete the old one, otherwise the gl nodes when saved will attempt to set the value // of a param with the potentially wrong type if (existing_param.type() != type) { this._deleteParam(existing_param.name()); } } else { // check that the param is spare, so that the ones generated by gl nodes are not generating an exception Poly.warn(`a param named ${paramName} already exists`, this.node); } } const param: ParamConstructorMap[T] = new constructor(this.node.scene(), this.node, { serializerClass: this.node.scene().paramSerializerClass(), }); param.options.set(options); param.setName(paramName); param.setInitValue(defaultValue as never); param.initComponents(); // set param value // and overriden options if (initData == null) { param.set(defaultValue as never); } else { // If is_expression_for_entities is true, we need to call param.set with default_value first, such as for attribCreate. // Otherwise, as it would fail if the attribute was a vector // since that attribute would have .value equal to {x: undefined, y: undefined, z:undefined} if (param.options.isExpressionForEntities()) { param.set(defaultValue as never); } if (initData.raw_input != null) { param.set(initData.raw_input as never); } else { if (initData.simple_data != null) { param.set(initData.simple_data as never); } else { if (initData.complex_data != null) { const raw_input = initData.complex_data.raw_input; if (raw_input) { param.set(raw_input as never); } else { param.set(defaultValue as never); } const overriden_options = initData.complex_data.overriden_options; if (overriden_options != null) { const keys = Object.keys(overriden_options); for (const key of keys) { param.options.setOption(key as keyof ParamOptions, overriden_options[key]); } } } } } } param.postOptionsInitialize(); param._setupNodeDependencies(this.node); this._paramsByName.set(param.name(), param as BaseParamType); // we add the components, so that we can access them with expressions like ch('ty') if (param.isMultiple() && param.components) { for (const component of param.components) { this._paramsByName.set(component.name(), component as BaseParamType); } } this._params_added_since_last_params_eval = true; param.scene().graphNodesController.notifyParamPathChanged(param); return param; } } private _updateCaches() { this._paramsList.splice(0, this._paramsList.length); this._paramsByName.forEach((param) => { this._paramsList.push(param); }); this._paramNames = this._paramsList.map((param) => param.name()); this._non_spare_params = this._paramsList.filter((p) => !p.options.isSpare()); this._spare_params = this._paramsList.filter((p) => p.options.isSpare()); this._non_spare_param_names = this._non_spare_params.map((p) => p.name()); this._spare_param_names = this._spare_params.map((p) => p.name()); } async _evalParam(param: BaseParamType) { if (param.isDirty()) { await param.compute(); if (param.states.error.active() && param.disposed() == false) { this.node.states.error.set(`param '${param.name()}' error: ${param.states.error.message()}`); } } } private _promises: Promise<void>[] = []; async evalParams(params: BaseParamType[]) { let dirtyParamsCount = 0; for (const param of params) { if (param.isDirty()) { dirtyParamsCount += 1; } } this._promises.length = dirtyParamsCount; let i = 0; for (const param of params) { if (param.isDirty()) { this._promises[i] = this._evalParam(param); i += 1; } } await Promise.all(this._promises); if (this.node.states.error.active()) { this.node._setContainer(null); } } paramsEvalRequired(): boolean { return this._params_node != null && (this._params_node.isDirty() || this._params_added_since_last_params_eval); } async evalAll() { if (this.paramsEvalRequired()) { await this.evalParams(this._paramsList); this._params_node?.removeDirtyState(); this._params_added_since_last_params_eval = false; } } // // // HOOKS // // onParamsCreated(hook_name: string, hook: PostCreateParamsHook) { if (this._params_created) { hook(); } else { if (this._post_create_params_hook_names && this._post_create_params_hook_names.includes(hook_name)) { Poly.error(`hook name ${hook_name} already exists`); return; } this._post_create_params_hook_names = this._post_create_params_hook_names || []; this._post_create_params_hook_names.push(hook_name); this._post_create_params_hooks = this._post_create_params_hooks || []; this._post_create_params_hooks.push(hook); } } addOnSceneLoadHook(param_name: string, method: OnSceneLoadHook) { this._on_scene_load_hook_names = this._on_scene_load_hook_names || []; this._on_scene_load_hooks = this._on_scene_load_hooks || []; if (!this._on_scene_load_hook_names.includes(param_name)) { this._on_scene_load_hook_names.push(param_name); this._on_scene_load_hooks.push(method); } else { Poly.warn(`hook with name ${param_name} already exists`, this.node); } } private _runPostCreateParamsHooks() { if (this._post_create_params_hooks) { for (const hook of this._post_create_params_hooks) { hook(); } } } runOnSceneLoadHooks() { if (this._on_scene_load_hooks) { for (const hook of this._on_scene_load_hooks) { hook(); } } } }