@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
833 lines (780 loc) • 23.1 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 {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();
// }
}