UNPKG

polygonjs-engine

Version:

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

635 lines (591 loc) 17.6 kB
import {BaseParamType} from '../_Base'; import {BaseNodeType} from '../../nodes/_Base'; import {ParamType} from '../../poly/ParamType'; import {ParamEvent} from '../../poly/ParamEvent'; import {NodeContext} from '../../poly/NodeContext'; import {CoreGraphNode} from '../../../core/graph/CoreGraphNode'; import {ColorConversion} from '../../../core/Color'; import {CoreType} from '../../../core/Type'; import {ArrayUtils} from '../../../core/ArrayUtils'; import {ObjectUtils} from '../../../core/ObjectUtils'; import {Boolean2, Number2, PolyDictionary} from '../../../types/GlobalTypes'; const CALLBACK_OPTION = 'callback'; const CALLBACK_STRING_OPTION = 'callbackString'; // const COLOR_OPTION = 'color'; const COMPUTE_ON_DIRTY = 'computeOnDirty'; const COOK_OPTION = 'cook'; const FILE_BROWSE_OPTION = 'fileBrowse'; const FILE_TYPE_OPTION = 'type'; // const EXPRESSION_ONLY_OPTION = 'expression_only'; const EXPRESSION = 'expression'; const FOR_ENTITIES = 'forEntities'; const LABEL = 'label'; const LEVEL = 'level'; const MENU = 'menu'; const ENTRIES = 'entries'; // const TYPE = 'type'; // const RADIO = 'radio'; const MULTILINE_OPTION = 'multiline'; const LANGUAGE_OPTION = 'language'; const NODE_SELECTION = 'nodeSelection'; const NODE_SELECTION_CONTEXT = 'context'; const NODE_SELECTION_TYPES = 'types'; const PARAM_SELECTION = 'paramSelection'; const DEPENDENT_ON_FOUND_NODE = 'dependentOnFoundNode'; const RANGE_OPTION = 'range'; const RANGE_LOCKED_OPTION = 'rangeLocked'; const STEP_OPTION = 'step'; const SPARE_OPTION = 'spare'; const TEXTURE_OPTION = 'texture'; const ENV_OPTION = 'env'; const HIDDEN_OPTION = 'hidden'; // const SHOW_LABEL_OPTION = 'show_label'; const FIELD_OPTION = 'field'; const VISIBLE_IF_OPTION = 'visibleIf'; const COLOR_CONVERSION = 'conversion'; export interface NumericParamOptionsMenuEntry { name: string; value: number; } export interface StringParamOptionsMenuEntry { name: string; value: string; } export interface MenuNumericParamOptions { menu?: { entries: NumericParamOptionsMenuEntry[]; }; } export interface MenuStringParamOptions { menu?: { entries: StringParamOptionsMenuEntry[]; }; } export enum StringParamLanguage { // JAVASCRIPT = 'javascript', TYPESCRIPT = 'typescript', // GLSL = 'glsl', } export enum FileType { TEXTURE = 'texture', GEOMETRY = 'geometry', } export type VisibleIfParamOptions = PolyDictionary<number | boolean>; interface BaseParamOptions { // cook cook?: boolean; // spare spare?: boolean; // visible hidden?: boolean; // show_label?: boolean; field?: boolean; visibleIf?: VisibleIfParamOptions | VisibleIfParamOptions[]; } interface ExpressionParamOptions { expression?: { forEntities?: boolean; }; } interface NumberParamOptions extends BaseParamOptions { range?: Number2; rangeLocked?: Boolean2; step?: number; } interface FileParamOptions { fileBrowse?: { type: FileType[]; }; } interface ComputeOnDirtyParamOptions { computeOnDirty?: boolean; } interface CallbackParamOptions { callback?: (node: BaseNodeType, param: BaseParamType) => any; callbackString?: string; } interface LabelParamOptions { label?: string; } interface ColorConversionOptions { conversion?: ColorConversion; } // actual param options export interface BooleanParamOptions extends BaseParamOptions, ComputeOnDirtyParamOptions, MenuNumericParamOptions, ExpressionParamOptions, CallbackParamOptions {} export interface ButtonParamOptions extends BaseParamOptions, CallbackParamOptions, LabelParamOptions {} export interface ColorParamOptions extends BaseParamOptions, ColorConversionOptions, ExpressionParamOptions, CallbackParamOptions, ComputeOnDirtyParamOptions {} export interface FloatParamOptions extends NumberParamOptions, MenuNumericParamOptions, ComputeOnDirtyParamOptions, ExpressionParamOptions, CallbackParamOptions {} export interface FolderParamOptions extends BaseParamOptions { level?: number; } export interface IntegerParamOptions extends NumberParamOptions, MenuNumericParamOptions, CallbackParamOptions {} export interface OperatorPathParamOptions extends BaseParamOptions, FileParamOptions, ComputeOnDirtyParamOptions, CallbackParamOptions { nodeSelection?: { context?: NodeContext; types?: Readonly<string[]>; }; dependentOnFoundNode?: boolean; paramSelection?: ParamType | boolean; } export interface RampParamOptions extends BaseParamOptions {} export interface SeparatorParamOptions extends BaseParamOptions {} export interface StringParamOptions extends BaseParamOptions, FileParamOptions, CallbackParamOptions, ExpressionParamOptions { multiline?: boolean; language?: StringParamLanguage; } interface VectorParamOptions extends BaseParamOptions, ExpressionParamOptions, CallbackParamOptions, ComputeOnDirtyParamOptions {} export interface Vector2ParamOptions extends VectorParamOptions {} export interface Vector3ParamOptions extends VectorParamOptions {} export interface Vector4ParamOptions extends VectorParamOptions {} export interface ParamOptions extends NumberParamOptions, ColorConversionOptions, ComputeOnDirtyParamOptions, FolderParamOptions, ExpressionParamOptions, ButtonParamOptions, FileParamOptions, MenuNumericParamOptions, StringParamOptions, OperatorPathParamOptions { texture?: { env?: boolean; }; } export class OptionsController { private _programatic_visible_state: boolean = true; private _options!: ParamOptions; private _default_options!: ParamOptions; constructor(private _param: BaseParamType) { // this._options = lodash_cloneDeep(this._default_options); } dispose() { this._options[CALLBACK_OPTION] = undefined; this._options[CALLBACK_STRING_OPTION] = undefined; this._visibility_graph_node?.dispose(); } set(options: ParamOptions) { this._default_options = options; this._options = ObjectUtils.cloneDeep(this._default_options); this.post_set_options(); } copy(options_controller: OptionsController) { this._default_options = ObjectUtils.cloneDeep(options_controller.default()); this._options = ObjectUtils.cloneDeep(options_controller.current()); this.post_set_options(); } set_option<K extends keyof ParamOptions>(name: K, value: ParamOptions[K]) { this._options[name] = value; if (this._param.components) { for (let component of this._param.components) { component.options.set_option(name, value); } } } private post_set_options() { this._handle_computeOnDirty(); } param() { return this._param; } node(): BaseNodeType { return this._param.node; } default() { return this._default_options; } current() { return this._options; } // utils has_options_overridden(): boolean { return !ObjectUtils.isEqual(this._options, this._default_options); } overridden_options(): ParamOptions { const overriden: ParamOptions = {}; const option_names = Object.keys(this._options) as Array<keyof ParamOptions>; for (let option_name of option_names) { if (!ObjectUtils.isEqual(this._options[option_name], this._default_options[option_name])) { const cloned_option = ObjectUtils.cloneDeep(this._options[option_name]); Object.assign(overriden, {[option_name]: cloned_option}); } } return overriden; } overridden_option_names(): Array<keyof ParamOptions> { return Object.keys(this.overridden_options()) as Array<keyof ParamOptions>; } // compute on dirty computeOnDirty(): boolean { return this._options[COMPUTE_ON_DIRTY] || false; } private _computeOnDirty_callback_added: boolean | undefined; private _handle_computeOnDirty() { if (this.computeOnDirty()) { if (!this._computeOnDirty_callback_added) { this.param().addPostDirtyHook('computeOnDirty', this._compute_param.bind(this)); this._computeOnDirty_callback_added = true; } } } private async _compute_param() { await this.param().compute(); } // callback has_callback() { return this._options[CALLBACK_OPTION] != null || this._options[CALLBACK_STRING_OPTION] != null; } private _callbackAllowed = false; allowCallback() { this._callbackAllowed = true; } execute_callback() { if (!this._callbackAllowed) { return; } if (!this.node()) { return; } // we only allow execution when scene is loaded // to avoid errors such as an operator_path param // executing its callback before the node it points to is created if (!this.node().scene().loadingController.loaded()) { return; } const callback = this.get_callback(); if (callback != null) { // not running the callback when a node is cooking prevents some event nodes from behaving as expected. // It may also prevent files such as the sop/file to reload correctly if its reload callback was called while it loads a file // if (!this.node.cookController.is_cooking) { const parent_param = this.param().parent_param; if (parent_param) { // if the param is a component of a MultipleParam, // we let the parent handle the callback. // The main reason is for material builder uniforms. // If the component executes the callback, the uniform that is expecting a vector // will be receiving a float. The reason is that the callback is created by the ParamConfig, and it is then passed down to the component unchanged. // I could maybe find a way so that the param config creates callback for the multiple param // and also for the components. But they would have to be assigned correctly by the multiple param parent_param.options.execute_callback(); } else { callback(this.node(), this.param()); } // } else { // console.warn(`node ${this.node.fullPath()} cooking, not running callback`, this.param.name); // } } } private get_callback() { if (this.has_callback()) { return (this._options[CALLBACK_OPTION] = this._options[CALLBACK_OPTION] || this.create_callback_from_string()); } } private create_callback_from_string() { const callbackString = this._options[CALLBACK_STRING_OPTION]; if (callbackString) { const callback_function = new Function('node', 'scene', 'window', 'location', callbackString); return () => { callback_function(this.node(), this.node().scene(), null, null); }; } } // color color_conversion() { return this._options[COLOR_CONVERSION]; } // cook makes_node_dirty_when_dirty() { let cook_options; // false as the dirty state will go through the parent param if (this.param().parent_param != null) { return false; } let value = true; if ((cook_options = this._options[COOK_OPTION]) != null) { value = cook_options; } return value; } // desktop file_browse_option() { return this._options[FILE_BROWSE_OPTION]; } file_browse_allowed(): boolean { return this.file_browse_option() != null; } file_browse_type(): FileType[] | null { const option = this.file_browse_option(); if (option) { return option[FILE_TYPE_OPTION]; } else { return null; } } // expression // get displays_expression_only() { // return this._options[EXPRESSION_ONLY_OPTION] === true; // } is_expression_for_entities(): boolean { const expr_option = this._options[EXPRESSION]; if (expr_option) { return expr_option[FOR_ENTITIES] || false; } return false; } // folder level() { return this._options[LEVEL] || 0; } // menu has_menu() { return this.menu_options() != null; } private menu_options() { return this._options[MENU]; } // private get menu_type() { // if(this.menu_options){ // return this.menu_options[TYPE]; // } // } menu_entries() { const options = this.menu_options(); if (options) { return options[ENTRIES]; } else { return []; } } has_menu_radio() { return this.has_menu(); //&& this.menu_options[TYPE] === RADIO; } // multiline is_multiline(): boolean { return this._options[MULTILINE_OPTION] === true; } language(): StringParamLanguage | undefined { return this._options[LANGUAGE_OPTION]; } is_code(): boolean { return this.language() != null; } // node selection node_selection_options() { return this._options[NODE_SELECTION]; } node_selection_context() { const options = this.node_selection_options(); if (options) { return options[NODE_SELECTION_CONTEXT]; } } node_selection_types() { const options = this.node_selection_options(); if (options) { return options[NODE_SELECTION_TYPES]; } } dependent_on_found_node() { if (DEPENDENT_ON_FOUND_NODE in this._options) { return this._options[DEPENDENT_ON_FOUND_NODE]; } else { return true; } } // param selection is_selecting_param() { return this.param_selection_options() != null; } param_selection_options() { return this._options[PARAM_SELECTION]; } param_selection_type() { const options = this.param_selection_options(); if (options) { const type_or_boolean = options; if (!CoreType.isBoolean(type_or_boolean)) { return type_or_boolean; } } } // range range(): Number2 { // cannot force range easily, as values are not necessarily from 0 to N // if(this.self.has_menu() && this.self.menu_entries()){ // return [0, this.self.menu_entries().length-1 ] // } else { return this._options[RANGE_OPTION] || [0, 1]; // } } step(): number | undefined { return this._options[STEP_OPTION]; } private range_locked(): Boolean2 { // if(this.self.has_menu() && this.self.menu_entries()){ // return [true, true] // } else { return this._options[RANGE_LOCKED_OPTION] || [false, false]; // } } ensure_in_range(value: number): number { const range = this.range(); if (value >= range[0] && value <= range[1]) { return value; } else { if (value < range[0]) { return this.range_locked()[0] === true ? range[0] : value; } else { return this.range_locked()[1] === true ? range[1] : value; } } } // spare is_spare(): boolean { return this._options[SPARE_OPTION] || false; } // texture texture_options() { return this._options[TEXTURE_OPTION]; } texture_as_env(): boolean { const texture_options = this.texture_options(); if (texture_options != null) { return texture_options[ENV_OPTION] === true; } return false; } // visible is_hidden(): boolean { return this._options[HIDDEN_OPTION] === true || this._programatic_visible_state === false; } is_visible(): boolean { return !this.is_hidden(); } set_visible_state(state: boolean) { this._options[HIDDEN_OPTION] = !state; this.param().emit(ParamEvent.VISIBLE_UPDATED); } // label label() { return this._options[LABEL]; } is_label_hidden(): boolean { const type = this.param().type(); return ( // this._options[SHOW_LABEL_OPTION] === false || type === ParamType.BUTTON || type === ParamType.SEPARATOR || (type === ParamType.BOOLEAN && this.is_field_hidden()) ); } is_field_hidden(): boolean { return this._options[FIELD_OPTION] === false; } // programatic visibility ui_data_depends_on_other_params(): boolean { return VISIBLE_IF_OPTION in this._options; } visibility_predecessors() { const visibility_options = this._options[VISIBLE_IF_OPTION]; if (!visibility_options) { return []; } let predecessor_names: string[] = []; if (CoreType.isArray(visibility_options)) { predecessor_names = ArrayUtils.uniq(visibility_options.map((options) => Object.keys(options)).flat()); } else { predecessor_names = Object.keys(visibility_options); } const node = this.param().node; return ArrayUtils.compact( predecessor_names.map((name) => { const param = node.params.get(name); if (param) { return param; } else { console.error( `param ${name} not found as visibility condition for ${this.param().name()} in node ${this.param().node.type()}` ); } }) ); } private _update_visibility_and_remove_dirty_bound = this.update_visibility_and_remove_dirty.bind(this); private _visibility_graph_node: CoreGraphNode | undefined; private _ui_data_dependency_set: boolean = false; set_ui_data_dependency() { // currently this is only called on request on a per-param and therefore per-node basis, not on scene load for the whole scene if (this._ui_data_dependency_set) { return; } this._ui_data_dependency_set = true; const predecessors = this.visibility_predecessors(); if (predecessors.length > 0) { this._visibility_graph_node = new CoreGraphNode(this.param().scene(), 'param_visibility'); for (let predecessor of predecessors) { this._visibility_graph_node.addGraphInput(predecessor); } this._visibility_graph_node.addPostDirtyHook( '_update_visibility_and_remove_dirty', this._update_visibility_and_remove_dirty_bound ); } } private update_visibility_and_remove_dirty() { this.update_visibility(); this.param().removeDirtyState(); } async update_visibility() { const options = this._options[VISIBLE_IF_OPTION]; if (options) { const params = this.visibility_predecessors(); const promises = params.map((p) => { if (p.isDirty()) { return p.compute(); } }); this._programatic_visible_state = false; await Promise.all(promises); if (CoreType.isArray(options)) { for (let options_set of options) { const satisfied_values = params.filter((param) => param.value == options_set[param.name()]); if (satisfied_values.length == params.length) { this._programatic_visible_state = true; } } } else { const satisfied_values = params.filter((param) => param.value == options[param.name()]); this._programatic_visible_state = satisfied_values.length == params.length; } this.param().emit(ParamEvent.VISIBLE_UPDATED); } } }