UNPKG

@polygonjs/polygonjs

Version:

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

833 lines (780 loc) 23.1 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 {isString, isArray, isFunction} from '../../../core/Type'; import {arrayDifference, arrayCompact, arrayUniq} from '../../../core/ArrayUtils'; import {objectCloneDeep, objectIsEqual} from '../../../core/ObjectUtils'; import {PolyScene} from '../../scene/PolyScene'; 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_BROWSE_EXTENSIONS = 'extensions'; // const EXPRESSION_ONLY_OPTION = 'expression_only'; const EXPRESSION = 'expression'; const FOR_ENTITIES = 'forEntities'; const LABEL = 'label'; const HIDE_LABEL = 'hideLabel'; const LEVEL = 'level'; const MENU = 'menu'; const MENU_STRING = 'menuString'; 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 OBJECT_MASK = 'objectMask'; const OBJECT_MASK_INPUT_INDEX = 'inputIndex'; // const PARAM_SELECTION = 'paramSelection'; const DEPENDENT_ON_FOUND_NODE = 'dependentOnFoundNode'; const DEPENDENT_ON_FOUND_PARAM = 'dependentOnFoundParam'; 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'; const SEPARATOR_BEFORE_OPTION = 'separatorBefore'; const SEPARATOR_AFTER_OPTION = 'separatorAfter'; const JOIN_TO_PREVIOUS_PARAM = 'joinToPreviousParam'; const AS_QUATERNION = 'asQuaternion'; export const PARAM_OPTION_NAMES = { CALLBACK_OPTION, }; const EDITABLE = 'editable'; export interface GenericParamOptionsMenuEntry<T> { name: string; value: T; } export type StringParamOptionsMenuEntry = GenericParamOptionsMenuEntry<string>; export type NumericParamOptionsMenuEntry = GenericParamOptionsMenuEntry<number>; export interface MenuNumericParamOptions { menu?: { entries: NumericParamOptionsMenuEntry[]; }; } export interface MenuStringParamOptions { menuString?: { entries: StringParamOptionsMenuEntry[]; }; } export enum StringParamLanguage { // JAVASCRIPT = 'javascript', CSS = 'css', GLSL = 'glsl', HTML = 'html', TYPESCRIPT = 'typescript', JSON = 'json', // GLSL = 'glsl', } export enum FileType { AUDIO = 'audio', TEXTURE_IMAGE = 'texture_image', TEXTURE_VIDEO = 'texture_video', GEOMETRY = 'geometry', FONT = 'font', SVG = 'svg', JSON = 'json', } export type VisibleIfParamOptions = PolyDictionary<number | boolean | string>; export interface BaseParamOptions { // cook cook?: boolean; // spare spare?: boolean; // visible hidden?: boolean; // show_label?: boolean; field?: boolean; visibleIf?: VisibleIfParamOptions | VisibleIfParamOptions[]; // separator separatorBefore?: boolean; separatorAfter?: boolean; joinToPreviousParam?: boolean; // editable editable?: boolean; } interface ExpressionParamOptions { expression?: { forEntities?: boolean; }; } interface NumberParamOptions extends BaseParamOptions { range?: Number2; rangeLocked?: Boolean2; step?: number; } interface FileParamOptions { fileBrowse?: { extensions: string[]; }; } interface ComputeOnDirtyParamOptions { computeOnDirty?: boolean; } interface CallbackParamOptions { callback?: (node: BaseNodeType, param: BaseParamType) => any; callbackString?: string; } interface LabelParamOptions { label?: string; } interface LabelVisibilityParamOptions { hideLabel?: boolean; } interface ColorConversionOptions { conversion?: ColorConversion; } // actual param options export interface BooleanParamOptions extends BaseParamOptions, ComputeOnDirtyParamOptions, MenuNumericParamOptions, ExpressionParamOptions, CallbackParamOptions, LabelParamOptions {} export interface ButtonParamOptions extends BaseParamOptions, CallbackParamOptions, LabelParamOptions {} export interface ColorParamOptions extends BaseParamOptions, LabelParamOptions, ColorConversionOptions, ExpressionParamOptions, CallbackParamOptions, ComputeOnDirtyParamOptions {} export interface FloatParamOptions extends NumberParamOptions, MenuNumericParamOptions, ComputeOnDirtyParamOptions, ExpressionParamOptions, CallbackParamOptions {} export interface FolderParamOptions extends BaseParamOptions { level?: number; } interface ObjectMask { inputIndex?: number; fromInputOnly?: boolean; } type ObjectMaskOptions = ObjectMask | boolean; export interface IntegerParamOptions extends NumberParamOptions, MenuNumericParamOptions, ComputeOnDirtyParamOptions, ExpressionParamOptions, CallbackParamOptions {} export interface NodePathParamOptions extends BaseParamOptions, FileParamOptions, ComputeOnDirtyParamOptions, CallbackParamOptions { nodeSelection?: { context?: NodeContext; types?: Readonly<string[]>; }; dependentOnFoundNode?: boolean; } export interface ParamPathParamOptions extends BaseParamOptions, FileParamOptions, ComputeOnDirtyParamOptions, CallbackParamOptions { dependentOnFoundParam?: boolean; paramSelection?: ParamType | boolean; } export interface RampParamOptions extends BaseParamOptions, LabelVisibilityParamOptions {} export interface SeparatorParamOptions extends BaseParamOptions {} export interface StringParamOptions extends BaseParamOptions, MenuStringParamOptions, FileParamOptions, CallbackParamOptions, ExpressionParamOptions, LabelVisibilityParamOptions { multiline?: boolean; language?: StringParamLanguage; objectMask?: ObjectMaskOptions; } interface VectorParamOptions extends BaseParamOptions, LabelParamOptions, ExpressionParamOptions, CallbackParamOptions, ComputeOnDirtyParamOptions {} export interface Vector2ParamOptions extends VectorParamOptions {} export interface Vector3ParamOptions extends VectorParamOptions {} interface QuaternionOptions { asQuaternion?: boolean; } export interface Vector4ParamOptions extends VectorParamOptions, QuaternionOptions {} export interface ParamOptions extends NumberParamOptions, ColorConversionOptions, ComputeOnDirtyParamOptions, FolderParamOptions, ExpressionParamOptions, ButtonParamOptions, FileParamOptions, MenuNumericParamOptions, StringParamOptions, NodePathParamOptions, ParamPathParamOptions, LabelVisibilityParamOptions, QuaternionOptions { texture?: { env?: boolean; }; } // type OptionChangeCallback = () => void; // we don't want to check if EDITABLE has been overriden, // as it is overriden dynamically for the gl nodes // and that override should therefore not be saved with the scene const NON_OVERRIDABLE_OPTIONS: Array<keyof ParamOptions> = [EDITABLE]; export class OptionsController { private _programaticVisibleState: boolean = true; private _options!: ParamOptions; private _default_options!: ParamOptions; constructor(private _param: BaseParamType) {} dispose() { try { // there is a bug where the _options is just a string // for builder params. And accessing generates an error this._options[CALLBACK_OPTION] = undefined; this._options[CALLBACK_STRING_OPTION] = undefined; } catch (err) {} this._visibility_graph_node?.dispose(); } set(options: ParamOptions) { if (isString(options)) { console.warn('options input invalid', options, typeof options); } this._default_options = options; this._options = objectCloneDeep(this._default_options); this.postSetOptions(); } copy(options_controller: OptionsController) { this._default_options = objectCloneDeep(options_controller.default()); this._options = objectCloneDeep(options_controller.current()); this.postSetOptions(); } setOption<K extends keyof ParamOptions>(optionName: K, value: ParamOptions[K]) { if (!this._validateOption(optionName, value)) { return; } this._options[optionName] = value; if (this._param.components) { for (const component of this._param.components) { component.options.setOption(optionName, value); } } // this._runOptionCallback(optionName); } private _validateOption<K extends keyof ParamOptions>(optionName: K, value: ParamOptions[K]) { if (optionName == CALLBACK_OPTION) { return isFunction(value); } return true; } private postSetOptions() { this._handleComputeOnDirty(); } param() { return this._param; } node(): BaseNodeType { return this._param.node; } default() { return this._default_options; } current() { return this._options; } // utils hasOptionsOverridden(): boolean { return !objectIsEqual(this._options, this._default_options); } overriddenOptions(): ParamOptions { const overriden: ParamOptions = {}; const optionNames = Object.keys(this._options) as Array<keyof ParamOptions>; const optionNamesToCheck: Array<keyof ParamOptions> = []; arrayDifference(optionNames, NON_OVERRIDABLE_OPTIONS, optionNamesToCheck); for (const optionName of optionNamesToCheck) { if (!objectIsEqual(this._options[optionName], this._default_options[optionName])) { const cloned_option = objectCloneDeep(this._options[optionName]); Object.assign(overriden, {[optionName]: cloned_option}); } } return overriden; } overriddenOptionNames(): Array<keyof ParamOptions> { return Object.keys(this.overriddenOptions()) as Array<keyof ParamOptions>; } // compute on dirty computeOnDirty(): boolean { return this._options[COMPUTE_ON_DIRTY] || false; } private _computeOnDirtyCallbackAdded: boolean | undefined; private _handleComputeOnDirty() { if (this.computeOnDirty()) { if (!this._computeOnDirtyCallbackAdded) { this.param().addPostDirtyHook('computeOnDirty', this._computeParam.bind(this)); this._computeOnDirtyCallbackAdded = true; } } } private async _computeParam() { await this.param().compute(); } // callback hasCallback() { return this._options[CALLBACK_OPTION] != null || this._options[CALLBACK_STRING_OPTION] != null; } private _callbackAllowed = false; allowCallback() { this._callbackAllowed = true; } async executeCallback() { if (!this._callbackAllowed) { return; } const node = this.node(); if (!node) { return; } const scene = node.scene(); if (!scene) { return; } const callback = this.getCallback(node, scene); if (!callback) { 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 (!scene.loadingController.loaded()) { return; } // 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 parentParam = this.param().parentParam(); if (parentParam) { // 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 parentParam.options.executeCallback(); } else { await callback(node, this.param()); } } private getCallback(node: BaseNodeType, scene: PolyScene) { if (this.hasCallback()) { return (this._options[CALLBACK_OPTION] = this._options[CALLBACK_OPTION] || this.createCallbackFromString(node, scene)); } } private createCallbackFromString(node: BaseNodeType, scene: PolyScene) { const callbackString = this._options[CALLBACK_STRING_OPTION]; if (callbackString) { const callbackFunction = new Function('node', 'scene', 'window', 'location', callbackString); return () => { callbackFunction(node, scene, null, null); }; } } // color colorConversion() { return this._options[COLOR_CONVERSION] || ColorConversion.NONE; } // cook makesNodeDirtyWhenDirty() { // false as the dirty state will go through the parent param if (this.param().parentParam() != null) { return false; } const cookOptions = this._options[COOK_OPTION]; if (cookOptions != null) { return cookOptions; } return true; } // desktop fileBrowseOption() { return this._options[FILE_BROWSE_OPTION]; } fileBrowseAllowed(): boolean { return this.fileBrowseOption() != null; } fileBrowseExtensions(): string[] | null { const option = this.fileBrowseOption(); if (option) { return option[FILE_BROWSE_EXTENSIONS]; } else { return null; } } // separator separatorBefore() { return this._options[SEPARATOR_BEFORE_OPTION]; } separatorAfter() { return this._options[SEPARATOR_AFTER_OPTION]; } joinToPreviousParam() { return this._options[JOIN_TO_PREVIOUS_PARAM]; } // editable setEditableState(state: boolean) { const currentState = this._options[EDITABLE]; const param = this.param(); if (currentState != state) { this._options[EDITABLE] = state; param.emit(ParamEvent.EDITABLE_UPDATED); } if (param.components) { for (const component of param.components) { component.options.setEditableState(state); } } } editable(): boolean { const state = this._options[EDITABLE]; if (state != null) { return state; } return true; } // expression // get displays_expression_only() { // return this._options[EXPRESSION_ONLY_OPTION] === true; // } isExpressionForEntities(): 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 hasMenu() { return this.menuOptions() != null || this.menuStringOptions() != null; } private menuOptions() { return this._options[MENU]; } private menuStringOptions() { return this._options[MENU_STRING]; } menuEntries(): Array<NumericParamOptionsMenuEntry | StringParamOptionsMenuEntry> { const options = this.menuOptions() || this.menuStringOptions(); if (options) { return options[ENTRIES]; } else { return []; } } ensureValueInMenuEntries(value: number): number { const options = this.menuOptions(); if (!options) { return value; } const entries = options[ENTRIES]; if (entries.length == 0) { return value; } for (const entry of entries) { if (value == entry.value) { return value; } } return entries[0].value; } // multiline isMultiline(): boolean { return this._options[MULTILINE_OPTION] === true; } language(): StringParamLanguage | undefined { return this._options[LANGUAGE_OPTION]; } isCode(): boolean { return this.language() != null; } // node selection nodeSelectionOptions() { return this._options[NODE_SELECTION]; } nodeSelectionContext() { const options = this.nodeSelectionOptions(); if (options) { return options[NODE_SELECTION_CONTEXT]; } } nodeSelectionTypes() { const options = this.nodeSelectionOptions(); if (options) { return options[NODE_SELECTION_TYPES]; } } displayObjectMaskSelection() { const value = this._options[OBJECT_MASK]; return value != null && value != false; } objectMaskInputIndex(): number { const value = this._options[OBJECT_MASK]; const input = value != null ? (value as ObjectMask)[OBJECT_MASK_INPUT_INDEX] : 0; return input || 0; } objectMaskFromInputOnly() { const value = this._options[OBJECT_MASK]; const fromInputOnly = (value as ObjectMask).fromInputOnly; return fromInputOnly != false; } dependentOnFoundNode() { if (DEPENDENT_ON_FOUND_NODE in this._options) { return this._options[DEPENDENT_ON_FOUND_NODE]; } else { return true; } } dependentOnFoundParam() { if (DEPENDENT_ON_FOUND_PARAM in this._options) { return this._options[DEPENDENT_ON_FOUND_PARAM]; } else { return true; } } // param selection isSelectingParam() { return this.param().type() == ParamType.PARAM_PATH; // return this.paramSelectionOptions() != null; } // paramSelectionOptions() { // return this._options[PARAM_SELECTION]; // } // paramSelectionType() { // const options = this.paramSelectionOptions(); // if (options) { // const type_or_boolean = options; // if (!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]; } asQuaternion() { return this._options[AS_QUATERNION] == true; } rangeLocked(): Boolean2 { // if(this.self.has_menu() && this.self.menu_entries()){ // return [true, true] // } else { return this._options[RANGE_LOCKED_OPTION] || [false, false]; // } } ensureInRange(value: number): number { const range = this.range(); if (value >= range[0] && value <= range[1]) { return value; } else { if (value < range[0]) { return this.rangeLocked()[0] === true ? range[0] : value; } else { return this.rangeLocked()[1] === true ? range[1] : value; } } } // spare isSpare(): boolean { return this._options[SPARE_OPTION] || false; } // texture textureOptions() { return this._options[TEXTURE_OPTION]; } textureAsEnv(): boolean { const texture_options = this.textureOptions(); if (texture_options != null) { return texture_options[ENV_OPTION] === true; } return false; } // visible isHidden(): boolean { return this._options[HIDDEN_OPTION] === true || this._programaticVisibleState === false; } isVisible(): boolean { return !this.isHidden(); } setVisibleState(state: boolean) { this._options[HIDDEN_OPTION] = !state; this.param().emit(ParamEvent.VISIBLE_UPDATED); } // label label() { return this._options[LABEL]; } isLabelHidden(): boolean { const type = this.param().type(); switch (type) { case ParamType.BUTTON: { return true; } case ParamType.BOOLEAN: { return this.isFieldHidden(); } case ParamType.RAMP: { return this._options[HIDE_LABEL] || false; } case ParamType.STRING: { return (this.isCode() && this._options[HIDE_LABEL]) || false; } } return false; } isFieldHidden(): boolean { return this._options[FIELD_OPTION] === false; } // programatic visibility uiDataDependsOnOtherParams(): boolean { return VISIBLE_IF_OPTION in this._options; } visibilityPredecessors() { const visibilityOptions = this._options[VISIBLE_IF_OPTION]; if (!visibilityOptions) { return []; } let predecessorNames: string[] = []; if (isArray(visibilityOptions)) { arrayUniq(visibilityOptions.map((options) => Object.keys(options)).flat(), predecessorNames); } else { predecessorNames = Object.keys(visibilityOptions); } const node = this.param().node; const params: BaseParamType[] = []; arrayCompact( predecessorNames.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()}` ); } }), params ); return params; } private _updateVisibilityAndRemoveDirtyBound = this.updateVisibilityAndRemoveDirty.bind(this); private _visibility_graph_node: CoreGraphNode | undefined; private _ui_data_dependency_set: boolean = false; setUiDataDependency() { // 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.visibilityPredecessors(); if (predecessors.length > 0) { this._visibility_graph_node = new CoreGraphNode(this.param().scene(), 'param_visibility'); for (const predecessor of predecessors) { this._visibility_graph_node.addGraphInput(predecessor); } this._visibility_graph_node.addPostDirtyHook( '_update_visibility_and_remove_dirty', this._updateVisibilityAndRemoveDirtyBound ); } } private updateVisibilityAndRemoveDirty() { this.updateVisibility(); this.param().removeDirtyState(); } async updateVisibility() { const options = this._options[VISIBLE_IF_OPTION]; if (options) { const node = this.param().node; const params = this.visibilityPredecessors(); const promises = params.map((p) => { if (p.isDirty()) { return p.compute(); } }); this._programaticVisibleState = false; await Promise.all(promises); if (isArray(options)) { for (const optionsSet of options) { const optionSetParamNames = Object.keys(optionsSet); const optionSetParams: BaseParamType[] = []; arrayCompact( optionSetParamNames.map((paramName) => node.params.get(paramName)), optionSetParams ); const satisfiedValues = optionSetParams.filter((param) => param.value == optionsSet[param.name()]); if (satisfiedValues.length == optionSetParams.length) { this._programaticVisibleState = true; } } } else { const satisfiedValues = params.filter((param) => param.value == options[param.name()]); this._programaticVisibleState = satisfiedValues.length == params.length; } this.param().emit(ParamEvent.VISIBLE_UPDATED); } } /* * * * */ // private _callbacksByOptionName: Map<keyof ParamOptions, OptionChangeCallback> | undefined; // onOptionChange<K extends keyof ParamOptions>(optionName: K, callback: OptionChangeCallback) { // this._callbacksByOptionName = this._callbacksByOptionName || new Map(); // this._callbacksByOptionName.set(optionName, callback); // } // private _runOptionCallback(optionName: keyof ParamOptions) { // if (!this._callbacksByOptionName) { // return; // } // const callback = this._callbacksByOptionName.get(optionName); // if (!callback) { // return; // } // callback(); // } }