polygonjs-engine
Version:
node-based webgl 3D engine https://polygonjs.com
635 lines (591 loc) • 17.6 kB
text/typescript
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);
}
}
}